Model: User¶
Represents a person registered with Molib. The User is the spine of authentication and (later) personnel concepts in the system.
Fields¶
| Field | Type | Constraints | Notes |
|---|---|---|---|
Id |
uuid |
PK, generated | |
CreatedAt |
timestamp with time zone |
NOT NULL, default now() UTC |
Set on insert. |
VerificationToken |
text |
NOT NULL | Cryptographically random, generated on signup. May be cleared/rotated after verification. |
VerifiedAt |
timestamp with time zone |
NULL, default NULL | Set when the user completes email verification. NULL ⇒ unverified. |
PasswordHash |
text |
NOT NULL | bcrypt hash (work factor ≥ 12). Plaintext is never persisted, logged, or returned by any endpoint. |
Invariants¶
VerificationTokenis unique whileVerifiedAt IS NULL. After verification it may be cleared, rotated, or kept for audit (project decision: clear it).- A
Usermust have at least oneEmailAuthenticationto be useful — sign-in always traverses anEmailAuthentication.Address. - Sign-in is denied while
VerifiedAt IS NULL.
Relationships¶
- One-to-many with
EmailAuthentication: each User may have many addresses; each address belongs to exactly one User. The FK is onEmailAuthentication.UserId → User.Id, configuredON DELETE RESTRICTandON UPDATE RESTRICT(project policy — deleting a User with addresses attached must fail loudly).
Notes¶
- The User row does not store an email itself — email lives on
EmailAuthentication. This lets us add multiple addresses per user later, and lets address changes happen without rewriting User rows. VerificationTokenis a column on User by design, not a row in a separate table (seeCLAUDE.md→ "Single-use security tokens are columns, not tables"). Repeat sign-up / verification requests overwrite the column rather than appending. This avoids a row-spam DoS vector on the unauthenticatedPOST /usersandPOST /users/verifyendpoints.- The same column-not-table rule will apply when password reset is added: expect a
PasswordResetTokencolumn onUser(plus anexpires-atcompanion if needed), not aPasswordResetTokentable. Tokens that need history (e.g.RefreshToken) are the exception and stay in their own table because they are issued post-authentication.