wulf-pulse/.planning/phases/07.1-user-timezone-fix-inserted-urgent/07.1-01-SUMMARY.md
lorentz 46ee1f6ade docs(07.1-01): complete user timezone column + Better Auth additionalField
- TZ-01 satisfied: per-user IANA timezone column added to "user" table
- session.user.timezone now exposed via Better Auth additionalFields
- Defaults: SQL DEFAULT 'UTC', app-level default reads process.env.DEFAULT_TIMEZONE
- No destructive ops; storage timezone of existing TIMESTAMP columns unchanged
2026-05-07 07:38:08 -04:00

8.8 KiB

phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
07.1-user-timezone-fix-inserted-urgent 01 auth
better-auth
postgres
migrations
timezone
iana
additionalFields
phase provides
02-mobile-shell-more-drawer Better Auth additionalFields pattern (role, requires_setup) — extended here
user.timezone column on Postgres `user` table (TEXT NOT NULL DEFAULT 'UTC', backfilled)
session.user.timezone available on every authenticated request via Better Auth additionalFields
Application-level default that reads process.env.DEFAULT_TIMEZONE (falls back to 'UTC')
07.1-02 (PUT /api/me/timezone — needs the column to write to)
07.1-03 (read-path fixes — needs session.user.timezone to compute day/week boundaries)
07.1-04 (client-side useTimezone hook — needs the field exposed on the session payload)
added patterns
Per-user IANA timezone stored as TEXT, NOT NULL DEFAULT 'UTC' — non-destructive ADD COLUMN IF NOT EXISTS
App-level default for additionalField reads process.env at boot, SQL default is the literal 'UTC'
created modified
migrations/083_add_user_timezone.sql
lib/auth.ts
SQL default is the literal 'UTC' (psql can't read process.env); app-level default in Better Auth's additionalField overrides at insert time
No CHECK constraint on the column — IANA validation happens in Plan 02's PUT /api/me/timezone route, not in Postgres
additionalField is read-only client-side by Better Auth default — writes go through the (planned) authenticated endpoint, not the session payload
Pattern: timezone migration is idempotent — ADD COLUMN IF NOT EXISTS + defensive UPDATE backfill, no destructive ops
Pattern: env-driven defaults for new auth fields use process.env.X || 'fallback' inside additionalFields.defaultValue
TZ-01
~2 min 2026-05-07

Phase 07.1 Plan 01: Add user.timezone column + Better Auth additionalField

Per-user IANA timezone column on the Better Auth user table, surfaced on session.user.timezone via Better Auth additionalFields with process.env.DEFAULT_TIMEZONE || 'UTC' as the app-level default.

Performance

  • Duration: ~2 min
  • Started: 2026-05-07T11:34:35Z
  • Completed: 2026-05-07T11:36:54Z
  • Tasks: 2
  • Files modified: 2 (1 created, 1 edited)

Accomplishments

  • Created migrations/083_add_user_timezone.sql — adds timezone TEXT NOT NULL DEFAULT 'UTC' to the user table with a defensive backfill and a COMMENT ON COLUMN documenting that storage timezone for all other date columns remains UTC.
  • Extended lib/auth.ts Better Auth user.additionalFields with timezone: { type: "string", defaultValue: process.env.DEFAULT_TIMEZONE || "UTC" }. The exported User type (typeof auth.$Infer.Session.user) automatically picks up the new field — no explicit type changes needed.
  • Verified npx tsc --noEmit --pretty is clean for lib/auth.ts.

Task Commits

Each task was committed atomically (parallel-executor mode, --no-verify):

  1. Task 1: Create migration 083_add_user_timezone.sql25e6b75 (feat)
  2. Task 2: Extend Better Auth additionalFields with timezone061f266 (feat)

Plan metadata commit will be added by the orchestrator after the wave completes.

Files Created/Modified

  • migrations/083_add_user_timezone.sql (created) — adds timezone column with default 'UTC', defensive UPDATE backfill, and a COMMENT documenting the storage-zone invariant.
  • lib/auth.ts (modified) — added timezone to user.additionalFields; existing role and requires_setup fields preserved unchanged.

Decisions Made

  • SQL default is literal 'UTC' not env-driven. Postgres can't read process.env. The application-layer default (Better Auth additionalField.defaultValue) is what reads process.env.DEFAULT_TIMEZONE. This means a user row created via raw SQL (e.g. seed data) gets 'UTC', while a user provisioned through Better Auth gets the operator-configured default. Existing rows are backfilled to 'UTC' regardless.
  • No CHECK constraint validating IANA names. Postgres can't evaluate Intl.supportedValuesOf and we don't want a hand-maintained allowlist drift over time. Validation lives in Plan 02's PUT /api/me/timezone route.
  • Migration filename is 083_* (next number after 082_company_scope.sql). Confirmed 082 is the latest; no number collision.

Deviations from Plan

None — plan executed exactly as written.

(One execution-environment hiccup is documented under Issues Encountered, but it required no changes to the plan or its content.)

Issues Encountered

  • Worktree path resolution. The first Write call to migrations/083_add_user_timezone.sql resolved to the canonical repo path (/opt/stacks/pulse/migrations/...) instead of the worktree path (/opt/stacks/pulse/.claude/worktrees/agent-ae6b0590a7cb003e4/migrations/...). Removed the stray file from the canonical repo and re-wrote into the worktree using the absolute worktree path. No code changes resulted; this only affected file placement during execution.
  • Worktree branch base was stale (db375fb). The worktree was branched from db375fb instead of the expected feature-branch HEAD (bee35e0). Rebased onto bee35e0 per the worktree protocol; rebase succeeded cleanly with no conflicts.

Gotchas / Notes for Next Plans

  • Operator must run scripts/apply-migrations.sh against the running DB. The migrations/*.sql files are only auto-applied by Postgres on first init (fresh volume). For an existing pulse DB, scripts/apply-migrations.sh is the sanctioned tool — confirmed it exists and supports both Docker (pulse-postgres) and local psql modes. The migration is idempotent (ADD COLUMN IF NOT EXISTS), so re-running is safe.
  • DEFAULT_TIMEZONE env var. New env var introduced by this plan. Optional. If unset, the app-level default falls back to 'UTC'. Recommend setting it in .env.local for the Wulf Consulting deploy (e.g. DEFAULT_TIMEZONE=America/New_York) so newly-provisioned users start in the org's primary zone instead of UTC.
  • Existing user rows are backfilled to 'UTC' regardless of DEFAULT_TIMEZONE. The env var only governs new row provisioning at the Better Auth layer. A separate one-shot script (out of scope for Phase 7.1) could update existing rows to the org default, but Plan 02's PUT endpoint will let users (or admins) set their own timezone going forward.
  • additionalFields are not client-writable by default in Better Auth 1.4. Plan 02's authenticated PUT route is the only sanctioned write path. Threat T-07.1-01-04 (Elevation of Privilege via session-payload write) is mitigated by this default, not by explicit code in this plan.
  • Storage timezone of every existing TIMESTAMP column is unchanged. This plan only adds a TEXT column; it does NOT touch created_at, updated_at, synced_at, or any other timestamp columns. Read-path adjustments in Plan 03 will apply timezone math at query/render time, not at storage.

User Setup Required

None — no external service configuration required.

The migration must be applied to the running database (scripts/apply-migrations.sh), but that's a deploy-time action handled by the operator/CI, not a per-environment external service setup.

Next Phase Readiness

  • Plan 02 unblocked: the user.timezone column exists and is exposed on session.user. The PUT /api/me/timezone route can now read the current value and write a new IANA string after validating it.
  • Plans 03 + 04 unblocked once Plan 02 ships: read-path fixes have a session field to consume; the client-side useTimezone hook has a stable shape.
  • Threat register status: T-07.1-01-01 through T-07.1-01-05 all addressed by the as-built (no SQL injection surface, additionalField is server-default, NOT NULL DEFAULT is O(1) on Postgres 11+, write path is gated, env-rewrite spoofing is out of scope by definition).

Self-Check: PASSED

Verified at /opt/stacks/pulse/.claude/worktrees/agent-ae6b0590a7cb003e4:

  • migrations/083_add_user_timezone.sql — FOUND
  • lib/auth.ts — modified, timezone field present with process.env.DEFAULT_TIMEZONE || "UTC" default
  • Commit 25e6b75 (Task 1) — FOUND
  • Commit 061f266 (Task 2) — FOUND
  • npx tsc --noEmit --pretty for lib/auth.ts — PASS (no errors)
  • All Task 1 acceptance criteria — PASS (file exists, ADD COLUMN, COMMENT, backfill, no destructive ops)
  • All Task 2 acceptance criteria — PASS (timezone field added, default is env-driven, existing fields preserved, no new imports)

Phase: 07.1-user-timezone-fix-inserted-urgent Completed: 2026-05-07