5.6 KiB
5.6 KiB
Phase 16: Multi-User Data Model - Context
Gathered: 2026-04-05 Status: Ready for planning
## Phase BoundaryAdd 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 DecisionsUser Identity Storage
- D-01: Create a thin local
userstable: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
requireAuthmiddleware resolves the authenticated identity to a localusers.idand 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 onname. Add composite unique constraint on(userId, name). Each user gets their own "Uncategorized" default category. - D-06:
settings: Change primary key fromkeyalone to composite(userId, key). Each user has their own settings (weightUnit, etc.). - D-07:
apiKeys: AdduserIdcolumn so middleware can resolve which user's data an API key grants access to. - D-08:
threadCandidatesandsetupItems: 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
userIdparameter. All queries includewhere(eq(table.userId, userId))for isolation. - D-10:
requireAuthmiddleware setsuserIdon context. Routes extractuserIdfrom context and pass to services.
Data Migration
- D-11: Migration script adds
userIdcolumn 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_refs>
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 CRUDsrc/server/services/category.service.ts— Category CRUD + unique constraintsrc/server/services/thread.service.ts— Thread + candidate CRUD + resolutionsrc/server/services/setup.service.ts— Setup CRUD + item syncsrc/server/services/totals.service.ts— Aggregate queries (weight/cost totals)src/server/services/csv.service.ts— CSV import/exportsrc/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 contextsrc/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 callstests/routes/*.test.ts— Route tests need userId in contexttests/mcp/tools.test.ts— MCP tests need userId scoping
Requirements
.planning/REQUIREMENTS.md— MULTI-01 through MULTI-06
</canonical_refs>
<code_context>
Existing Code Insights
Reusable Assets
- Service DI pattern (db as first param) — extend with userId as second param
requireAuthmiddleware — 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, ...)— becomesfunctionName(db, userId, ...) - Route context:
c.get("db")— addc.get("userId") - Async Postgres: All services already use
awaitwith 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 resolutionsrc/server/index.ts— Where middleware is appliedsrc/db/schema.ts— All table definitions need userId columntests/helpers/db.ts— Test DB helper needs user seed
</code_context>
## Specific IdeasNo specific requirements — open to standard approaches for multi-tenant data isolation with Drizzle ORM.
## Deferred IdeasNone — discussion stayed within phase scope
Phase: 16-multi-user-data-model Context gathered: 2026-04-05