Users and roles
Identity records, government roles, interest profiles, and notification preferences
This group answers “who is acting?” A User can represent an industry contact or a government employee. The same table supports both audiences, while nullable commandId and organizationId connect the user to the side of the platform they belong to.
User
| Column | Prisma type | Required? | Default | Meaning |
|---|---|---|---|---|
id | String | Yes | cuid() | Stable identifier used by sessions and foreign keys. |
email | String | Yes | None; unique | Login address. Authentication lowercases and trims credentials before lookup. |
passwordHash | String | Yes | None | Bcrypt hash; plaintext passwords are never stored. |
name | String | Yes | None | Display name shown in government and audit contexts. |
role | Role | Yes | INDUSTRY | Role-based access decision. |
mustChangePassword | Boolean | Yes | false | Forces an admin-created account through the password-change page before other pages and APIs can be used. |
sessionVersion | Int | Yes | 0 | Incremented when the account credential changes; JWTs with an older version are revoked on refresh. |
commandId | String? | No | NULL | Government command assignment. |
title | String? | No | NULL | Government title or position. |
organizationId | String? | No | NULL | Industry organization assignment. |
createdAt | DateTime | Yes | now() | Account creation time. |
updatedAt | DateTime | Yes | @updatedAt | Last account-row update. |
The model also exposes relation fields: command, organization, one interestProfile, many notificationSettings, many notifications, created engagements, engagement notes, organization dialogue notes, announcements, and reviewed submissions. Relation fields are typed Prisma conveniences, not duplicated scalar columns.
Role enum
| Value | Who it represents | Practical authority |
|---|---|---|
INDUSTRY | Industry organization contact | Own organization, own submissions, public calls and announcements. |
GOV_READONLY | Government viewer | Government read access where route policy allows; no publishing or administrative mutation. |
GOV_POC | Government point of contact (POC) | Receives routing notifications and participates in government workflows allowed by the route. |
GOV_COMMAND_LEAD | Command-level engagement lead | Own-command scope; can perform command-level government work but does not inherit child commands. |
GOV_ADMIN | Enterprise program administrator | Enterprise visibility and administrative functions, but not command-tree reorganization. |
GOV_SYSTEM_ADMIN | Enterprise system administrator | Enterprise visibility plus command hierarchy creation, editing, deletion, and reparenting. |
Role values are database enum values, not free-form strings. src/lib/auth.ts defines the government-role allowlist used by requireGovSession().
InterestProfile
| Column | Prisma type | Required? | Default | Meaning |
|---|---|---|---|---|
id | String | Yes | cuid() | Profile identifier. |
userId | String | Yes; unique | None | One-to-one link to the government user. |
techTags | String | Yes | None | Lowercase comma-separated areas of interest. |
keywords | String | Yes | None | Comma-separated free-text terms used by matching. |
portfolioNodeId | String? | No | NULL | Optional primary portfolio node assignment. |
The profile is intentionally small. A POC can be assigned to a portfolio node and still use free-form tags or keywords. routeSubmission checks both the matched node and overlapping profile terms before creating a notification.
NotificationSetting
| Column | Prisma type | Required? | Default | Meaning |
|---|---|---|---|---|
id | String | Yes | cuid() | Preference-row identifier. |
userId | String | Yes | None | Owner of the preference. |
kind | NotificationKind | Yes | None | Notification category controlled by this row. |
enabled | Boolean | Yes | true | Whether this category is enabled. |
techTags | String | Yes | "" | Optional lowercase comma-separated filter. |
@@unique([userId, kind]) means a user has at most one setting per category. Missing rows behave as enabled in src/lib/notify.ts; the settings GET endpoint materializes defaults for the UI.
Identity relationships
Example interpretation
poc.ai@navy.mil is a government POC assigned to NIWC Pacific with an AI/ML portfolio node and corresponding interest tags. A generated industry contact such as contact@pelagic-dynamics.example.com has role = INDUSTRY and an organizationId pointing to Pelagic Dynamics, but no commandId. The nullable columns are what allow both records to share one model without pretending an industry contact belongs to a Navy command.
Constraints and operational notes
email is unique, so registration and admin-user creation must handle a conflict rather than create a second identity. userId is unique on InterestProfile, making that relation one-to-one. Notification settings use a compound unique key by user and kind, so PUT is naturally an upsert.
passwordHash has no default because every account must come from an explicit registration, seed, or admin creation path. role does have a default of INDUSTRY, which is safe for public registration but means government role assignment must be deliberate.
mustChangePassword is the rotation gate for temporary admin-issued credentials. sessionVersion is the revocation counter: changing a password increments it, so every live JWT issued under the previous version becomes stale, including the caller's session on its next refresh.
A deleted or changed command assignment does not alter historical audit rows. Session authorization refreshes current role and command values on subsequent requests; the database remains the authority for present access.