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
This commit is contained in:
parent
061f266b18
commit
46ee1f6ade
1 changed files with 130 additions and 0 deletions
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
phase: 07.1-user-timezone-fix-inserted-urgent
|
||||
plan: 01
|
||||
subsystem: auth
|
||||
tags: [better-auth, postgres, migrations, timezone, iana, additionalFields]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 02-mobile-shell-more-drawer
|
||||
provides: Better Auth additionalFields pattern (role, requires_setup) — extended here
|
||||
provides:
|
||||
- "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')"
|
||||
affects:
|
||||
- 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)
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: [] # no new libs — uses existing Better Auth additionalFields surface
|
||||
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'"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- migrations/083_add_user_timezone.sql
|
||||
modified:
|
||||
- lib/auth.ts
|
||||
|
||||
key-decisions:
|
||||
- "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"
|
||||
|
||||
patterns-established:
|
||||
- "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"
|
||||
|
||||
requirements-completed: [TZ-01]
|
||||
|
||||
# Metrics
|
||||
duration: ~2 min
|
||||
completed: 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.sql** — `25e6b75` (feat)
|
||||
2. **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) — 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*
|
||||
Loading…
Add table
Add a link
Reference in a new issue