Skip to content

feat(store): add users.email_verified_at column + model plumbing (TASK-1935)#805

Merged
xarmian merged 1 commit into
mainfrom
feat/task-1935-email-verified-column
Jul 4, 2026
Merged

feat(store): add users.email_verified_at column + model plumbing (TASK-1935)#805
xarmian merged 1 commit into
mainfrom
feat/task-1935-email-verified-column

Conversation

@xarmian

@xarmian xarmian commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Wave 1 of PLAN-1933 (Pad Cloud email verification). Pure infra, no behavior change — nothing reads the column until Wave 3, so this is mergeable early behind the (not-yet-mounted) middleware.

What

  • Migration 070_email_verified.sql (SQLite) + 048_email_verified.sql (Postgres): add nullable users.email_verified_at TEXT, mirroring disabled_at. SQLite ALTER TABLE ADD COLUMN without IF NOT EXISTS (SQLite rejects it here); Postgres with IF NOT EXISTS.
  • UNCONDITIONAL backfill: every existing row is set to verified (RFC3339 Z-suffixed via strftime / to_char) so no existing / OAuth / self-host account is write-locked on deploy. This is inverted vs password_set's conditional (OAuth-aware) backfill — there is no "was this user ever verified?" signal, and the correct answer for every pre-existing account is "verified".
  • SAFE default = verified (DR-3): CreateUser / CreateOAuthUser write a verified timestamp unless a new explicit UserCreate.Unverified field requests otherwise. Only the future cloud self-serve branch (Wave 3) will set it, so a missed call site fails SAFE (verified), not write-locked.
  • Model: models.User.EmailVerifiedAt + IsEmailVerified() (mirrors IsDisabled()); UserCreate.Unverified (zero value → verified).
  • Both scan sites updated: scanUser AND the inline scan in SearchUsers (missing the second breaks the admin user list at runtime with a column/target mismatch).
  • Session payload: derived email_verified bool in sessionUserPayload for a later web wave.

Tests

  • Backfill verifies pre-existing NULL rows (runs the real migration DML, both dialects).
  • Fresh CreateUser verified by default; explicit Unverified: true persists as NULL; OAuth users verified.
  • Both scan paths (scanUser + SearchUsers) return the field for verified and unverified users.
  • IsEmailVerified() unit test.

Gates

  • make check — green.
  • make test-pg — green (dual-dialect verified; SQLite passing does not prove Postgres).
  • Codex review — CLEAN.

https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST

…K-1935)

Wave 1 of PLAN-1933 (email verification). Pure infra — nothing reads the
column until Wave 3, so this is behaviourally a no-op and mergeable early.

- Migration 070 (SQLite) / 048 (Postgres): add nullable email_verified_at
  TEXT, mirroring disabled_at. UNCONDITIONALLY backfill every existing row
  to verified (RFC3339 'Z'-suffixed) so no existing / OAuth / self-host
  account is write-locked on deploy (inverted vs password_set's conditional
  backfill). SQLite ALTER without IF NOT EXISTS; Postgres with it.
- SAFE default = verified (DR-3): CreateUser / CreateOAuthUser write a
  verified timestamp unless UserCreate.Unverified is explicitly requested
  (only the future cloud self-serve branch will set that). A missed call
  site fails SAFE (verified), not write-locked.
- models.User.EmailVerifiedAt + IsEmailVerified() (mirror IsDisabled).
- Update userColumns + BOTH scan sites (scanUser AND the inline SearchUsers
  scan) so the admin user list keeps working.
- Expose derived email_verified bool in sessionUserPayload for a later wave.

Gates: make check + make test-pg both green (dual-dialect verified).

Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST
@xarmian
xarmian merged commit 6a63fba into main Jul 4, 2026
4 checks passed
@xarmian
xarmian deleted the feat/task-1935-email-verified-column branch July 4, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant