# Phase 16: Multi-User Data Model - Context **Gathered:** 2026-04-05 **Status:** Ready for planning ## Phase Boundary Add user ownership to all user-created entities (items, categories, threads, setups, settings) and enforce complete cross-user data isolation. Every query must be scoped to the authenticated user. MCP tools operate within the authenticated user's scope. ## Implementation Decisions ### User Identity Storage - **D-01:** Create a thin local `users` table: `id` (serial integer PK), `logtoSub` (text, unique, not null), `createdAt` (timestamp). Auto-created on first OIDC login via upsert. - **D-02:** All entity tables reference `users.id` (integer FK) — not the Logto sub string directly. Integer FKs are more efficient for joins across 6+ tables. - **D-03:** The `requireAuth` middleware resolves the authenticated identity to a local `users.id` and sets it on the Hono context (e.g., `c.set("userId", userId)`). ### Schema Changes - **D-04:** Add `userId` (integer, NOT NULL, FK → users.id) column to: `items`, `categories`, `threads`, `setups`, `settings`, `apiKeys`. - **D-05:** `categories`: Drop global unique constraint on `name`. Add composite unique constraint on `(userId, name)`. Each user gets their own "Uncategorized" default category. - **D-06:** `settings`: Change primary key from `key` alone to composite `(userId, key)`. Each user has their own settings (weightUnit, etc.). - **D-07:** `apiKeys`: Add `userId` column so middleware can resolve which user's data an API key grants access to. - **D-08:** `threadCandidates` and `setupItems`: No userId needed — they inherit ownership through their parent thread/setup FK. ### Service Layer Changes - **D-09:** Every service function that reads or writes user-owned data gains a `userId` parameter. All queries include `where(eq(table.userId, userId))` for isolation. - **D-10:** `requireAuth` middleware sets `userId` on context. Routes extract `userId` from context and pass to services. ### Data Migration - **D-11:** Migration script adds `userId` column with a temporary default, then updates all existing rows to user ID 1 (the first registered user), then removes the default and sets NOT NULL. - **D-12:** Create "Uncategorized" category per-user on first login (or lazily when needed). ### MCP Tool Scoping - **D-13:** MCP tools resolve userId from the authenticated token (API key → userId lookup, or Bearer token → userId). All tool operations are scoped to that user. ### Claude's Discretion - Exact migration SQL approach (single migration vs multi-step) - Whether to use Drizzle's `.where()` chaining or a helper function for userId scoping - Default category creation strategy (eager on first login vs lazy on first item creation) - Whether thread resolution should check that the target category belongs to the same user - Order of service file changes (all at once vs table-by-table) ## Canonical References **Downstream agents MUST read these before planning or implementing.** ### Database Schema - `src/db/schema.ts` — Current schema (no userId columns yet) - `drizzle-pg/` — PostgreSQL migration directory ### Services (all need userId parameter) - `src/server/services/item.service.ts` — Item CRUD - `src/server/services/category.service.ts` — Category CRUD + unique constraint - `src/server/services/thread.service.ts` — Thread + candidate CRUD + resolution - `src/server/services/setup.service.ts` — Setup CRUD + item sync - `src/server/services/totals.service.ts` — Aggregate queries (weight/cost totals) - `src/server/services/csv.service.ts` — CSV import/export - `src/server/services/auth.service.ts` — API key management (needs userId) ### Routes (all need userId from context) - `src/server/routes/*.ts` — All route files pass userId to services ### Middleware - `src/server/middleware/auth.ts` — requireAuth resolves userId onto context - `src/server/services/auth.service.ts` — API key → userId lookup ### MCP - `src/server/mcp/index.ts` — MCP tool handlers need userId scoping ### Tests - `tests/services/*.test.ts` — All service tests need userId in calls - `tests/routes/*.test.ts` — Route tests need userId in context - `tests/mcp/tools.test.ts` — MCP tests need userId scoping ### Requirements - `.planning/REQUIREMENTS.md` — MULTI-01 through MULTI-06 ## Existing Code Insights ### Reusable Assets - Service DI pattern (db as first param) — extend with userId as second param - `requireAuth` middleware — extend to resolve and set userId on context - Drizzle `eq()` where clauses — same pattern, add userId condition - `createTestDb()` helper — extend to seed a test user ### Established Patterns - **Service DI**: `functionName(db, ...)` — becomes `functionName(db, userId, ...)` - **Route context**: `c.get("db")` — add `c.get("userId")` - **Async Postgres**: All services already use `await` with Drizzle (from Phase 14) - **Test isolation**: PGlite per-test — add user seed to `createTestDb()` ### Integration Points - `src/server/middleware/auth.ts` — Primary point for userId resolution - `src/server/index.ts` — Where middleware is applied - `src/db/schema.ts` — All table definitions need userId column - `tests/helpers/db.ts` — Test DB helper needs user seed ## Specific Ideas No specific requirements — open to standard approaches for multi-tenant data isolation with Drizzle ORM. ## Deferred Ideas None — discussion stayed within phase scope --- *Phase: 16-multi-user-data-model* *Context gathered: 2026-04-05*