Skip to content

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

  • VerificationToken is unique while VerifiedAt IS NULL. After verification it may be cleared, rotated, or kept for audit (project decision: clear it).
  • A User must have at least one EmailAuthentication to be useful — sign-in always traverses an EmailAuthentication.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 on EmailAuthentication.UserId → User.Id, configured ON DELETE RESTRICT and ON 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.
  • VerificationToken is a column on User by design, not a row in a separate table (see CLAUDE.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 unauthenticated POST /users and POST /users/verify endpoints.
  • The same column-not-table rule will apply when password reset is added: expect a PasswordResetToken column on User (plus an expires-at companion if needed), not a PasswordResetToken table. Tokens that need history (e.g. RefreshToken) are the exception and stay in their own table because they are issued post-authentication.