--- phase: 16-multi-user-data-model plan: 01 subsystem: database tags: [drizzle, pgTable, multi-user, userId, postgresql, auth-middleware] requires: - phase: 14-postgresql-migration provides: PostgreSQL infrastructure and PGlite test setup - phase: 15-external-authentication provides: OIDC auth via Logto, API key and OAuth Bearer auth methods provides: - users table with logtoSub for OIDC mapping - userId FK columns on all entity tables (items, categories, threads, setups, apiKeys, oauthTokens) - composite unique constraint on categories(userId, name) - composite primary key on settings(userId, key) - requireAuth middleware resolving userId onto Hono context - getOrCreateUser upsert function for OIDC login - getOrCreateUncategorized lazy category creation - test helper returning { db, userId } with seeded user affects: [16-02, 16-03, 16-04, services, routes, mcp-tools, tests] tech-stack: added: [] patterns: [userId-on-context, per-user-data-isolation, lazy-uncategorized-creation, upsert-on-first-login] key-files: created: - drizzle-pg.config.ts - drizzle-pg/0000_thankful_loners.sql modified: - src/db/schema.ts - src/db/seed.ts - src/server/middleware/auth.ts - src/server/services/auth.service.ts - src/server/services/oauth.service.ts - src/server/services/category.service.ts - src/server/index.ts - tests/helpers/db.ts key-decisions: - "All API routes require auth (no GET bypass) so userId is always available for per-user scoping" - "OAuth service functions converted from sync (.get/.run) to async (await) for pg compatibility" - "getOrCreateUncategorized placed in category.service.ts since it is category-related" patterns-established: - "userId resolution: requireAuth sets c.set('userId', ...) for all three auth methods" - "verifyApiKey/verifyAccessToken return { userId } | null instead of boolean" - "createTestDb returns { db, userId } -- all tests must destructure" - "Lazy per-user Uncategorized category creation on first OIDC login" requirements-completed: [MULTI-01, MULTI-04, MULTI-06] duration: 8min completed: 2026-04-05 --- # Phase 16 Plan 01: Multi-User Data Model Foundation Summary **pgTable schema with users table, userId FK on 6 entity tables, composite constraints, and auth middleware resolving userId for all auth methods** ## Performance - **Duration:** 8 min - **Started:** 2026-04-05T08:31:24Z - **Completed:** 2026-04-05T08:39:00Z - **Tasks:** 3 - **Files modified:** 10 ## Accomplishments - Migrated entire schema.ts from sqlite-core to pg-core (pgTable, serial, timestamp, doublePrecision) - Added users table with logtoSub unique identifier for OIDC mapping and userId FK to items, categories, threads, setups, apiKeys, oauthTokens - Auth middleware now resolves userId for API key, Bearer token, and OIDC session; all routes require auth - Test infrastructure returns { db, userId } with seeded user and createSecondTestUser helper ## Task Commits Each task was committed atomically: 1. **Task 1: Migrate schema.ts to pgTable and add users table + userId columns** - `91e93a3` (feat) 2. **Task 2: Update auth middleware and auth services to resolve userId** - `b6d562f` (feat) 3. **Task 3: Update test helper to seed user and return { db, userId }** - `050478c` (feat) ## Files Created/Modified - `src/db/schema.ts` - Rewritten from sqlite-core to pg-core; users table, userId columns, composite constraints - `src/db/seed.ts` - Emptied global seed; per-user categories created lazily - `src/server/middleware/auth.ts` - Rewritten to resolve userId for all 3 auth methods - `src/server/services/auth.service.ts` - Rewritten: getOrCreateUser, verifyApiKey returns userId, scoped API key CRUD - `src/server/services/oauth.service.ts` - Rewritten: all functions async, verifyAccessToken returns userId, generateTokens accepts userId - `src/server/services/category.service.ts` - Added getOrCreateUncategorized helper - `src/server/index.ts` - Removed GET bypass; all API routes require auth - `tests/helpers/db.ts` - PGlite-based, seeds user, returns { db, userId }, createSecondTestUser helper - `drizzle-pg.config.ts` - Drizzle config for PostgreSQL dialect - `drizzle-pg/0000_thankful_loners.sql` - Generated migration with full schema ## Decisions Made - All API routes require auth (removed GET bypass) so userId is always available on context for per-user data scoping - OAuth service functions converted from synchronous (.get/.run/.all) to async/await for PostgreSQL compatibility - getOrCreateUncategorized placed in category.service.ts since it is category-domain logic - Old user/session management functions removed from auth.service.ts (replaced by Logto OIDC) ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing Critical] Converted all oauth.service.ts functions to async** - **Found during:** Task 2 (auth service updates) - **Issue:** All oauth.service functions used synchronous .get()/.run()/.all() calls from bun-sqlite; these do not work with pg/PGlite which is async-only - **Fix:** Rewrote all oauth.service functions to use async/await with array destructuring instead of .get() - **Files modified:** src/server/services/oauth.service.ts - **Verification:** Code compiles correctly with pg-core types - **Committed in:** b6d562f (Task 2 commit) **2. [Rule 3 - Blocking] Created drizzle-pg.config.ts for migration generation** - **Found during:** Task 1 (schema migration) - **Issue:** Existing drizzle.config.ts was SQLite-only; needed PostgreSQL config to generate migrations - **Fix:** Created drizzle-pg.config.ts pointing to drizzle-pg/ output directory - **Files modified:** drizzle-pg.config.ts (new) - **Verification:** Migration generated successfully with 12 tables - **Committed in:** 91e93a3 (Task 1 commit) --- **Total deviations:** 2 auto-fixed (1 missing critical, 1 blocking) **Impact on plan:** Both fixes essential for pg compatibility. No scope creep. ## Issues Encountered None ## Known Stubs None - all data model changes are structural (schema, middleware, test infrastructure). No UI rendering involved. ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - Schema foundation complete with users table and userId columns on all entity tables - Auth middleware resolves userId for all auth methods - Test helper ready with seeded user - Next: Plan 16-02 updates all service files to accept userId parameter and filter queries - Note: createTestDb return type changed from `db` to `{ db, userId }` -- existing tests will need updating in Plan 16-04 --- *Phase: 16-multi-user-data-model* *Completed: 2026-04-05*