- 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
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 |
|
|
|
|
|
|
|
|
|
~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— addstimezone TEXT NOT NULL DEFAULT 'UTC'to theusertable with a defensive backfill and aCOMMENT ON COLUMNdocumenting that storage timezone for all other date columns remains UTC. - Extended
lib/auth.tsBetter Authuser.additionalFieldswithtimezone: { type: "string", defaultValue: process.env.DEFAULT_TIMEZONE || "UTC" }. The exportedUsertype (typeof auth.$Infer.Session.user) automatically picks up the new field — no explicit type changes needed. - Verified
npx tsc --noEmit --prettyis clean forlib/auth.ts.
Task Commits
Each task was committed atomically (parallel-executor mode, --no-verify):
- Task 1: Create migration 083_add_user_timezone.sql —
25e6b75(feat) - Task 2: Extend Better Auth additionalFields with timezone —
061f266(feat)
Plan metadata commit will be added by the orchestrator after the wave completes.
Files Created/Modified
migrations/083_add_user_timezone.sql(created) — addstimezonecolumn with default'UTC', defensive UPDATE backfill, and a COMMENT documenting the storage-zone invariant.lib/auth.ts(modified) — addedtimezonetouser.additionalFields; existingroleandrequires_setupfields preserved unchanged.
Decisions Made
- SQL default is literal
'UTC'not env-driven. Postgres can't readprocess.env. The application-layer default (Better AuthadditionalField.defaultValue) is what readsprocess.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.supportedValuesOfand we don't want a hand-maintained allowlist drift over time. Validation lives in Plan 02's PUT/api/me/timezoneroute. - Migration filename is
083_*(next number after082_company_scope.sql). Confirmed082is 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
Writecall tomigrations/083_add_user_timezone.sqlresolved 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 fromdb375fbinstead of the expected feature-branch HEAD (bee35e0). Rebased ontobee35e0per the worktree protocol; rebase succeeded cleanly with no conflicts.
Gotchas / Notes for Next Plans
- Operator must run
scripts/apply-migrations.shagainst the running DB. Themigrations/*.sqlfiles are only auto-applied by Postgres on first init (fresh volume). For an existing pulse DB,scripts/apply-migrations.shis the sanctioned tool — confirmed it exists and supports both Docker (pulse-postgres) and localpsqlmodes. The migration is idempotent (ADD COLUMN IF NOT EXISTS), so re-running is safe. DEFAULT_TIMEZONEenv var. New env var introduced by this plan. Optional. If unset, the app-level default falls back to'UTC'. Recommend setting it in.env.localfor 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 ofDEFAULT_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. additionalFieldsare 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
TEXTcolumn; it does NOT touchcreated_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.timezonecolumn exists and is exposed onsession.user. The PUT/api/me/timezoneroute 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
useTimezonehook 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— FOUNDlib/auth.ts— modified,timezonefield present withprocess.env.DEFAULT_TIMEZONE || "UTC"default- Commit
25e6b75(Task 1) — FOUND - Commit
061f266(Task 2) — FOUND npx tsc --noEmit --prettyforlib/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