docs(16): capture phase context
This commit is contained in:
126
.planning/phases/16-multi-user-data-model/16-CONTEXT.md
Normal file
126
.planning/phases/16-multi-user-data-model/16-CONTEXT.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Phase 16: Multi-User Data Model - Context
|
||||
|
||||
**Gathered:** 2026-04-05
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## 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.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## 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)
|
||||
|
||||
</decisions>
|
||||
|
||||
<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 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
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## 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
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
No specific requirements — open to standard approaches for multi-tenant data isolation with Drizzle ORM.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 16-multi-user-data-model*
|
||||
*Context gathered: 2026-04-05*
|
||||
@@ -0,0 +1,85 @@
|
||||
# Phase 16: Multi-User Data Model - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-04-05
|
||||
**Phase:** 16-multi-user-data-model
|
||||
**Areas discussed:** User ID Representation, Existing Data Migration, Category Uniqueness, Settings Scope, API Key Ownership
|
||||
**Mode:** --auto --batch (all decisions auto-selected)
|
||||
|
||||
---
|
||||
|
||||
## User ID Representation
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Local users table with integer ID | Thin table mapping Logto sub to auto-increment integer, all FKs use integer | ✓ |
|
||||
| Logto sub string directly | Store Logto sub (string UUID) as FK on every entity table | |
|
||||
| Mapping table without FK | Store logtoSub on entities, join manually | |
|
||||
|
||||
**User's choice:** Local users table with integer ID (auto-selected)
|
||||
**Notes:** Integer FKs are more efficient for joins. Thin table auto-creates on first OIDC login.
|
||||
|
||||
---
|
||||
|
||||
## Existing Data Migration
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Migration assigns all rows to user ID 1 | Add userId column, set all existing data to first user | ✓ |
|
||||
| Prompt for Logto sub during migration | Interactive script asks for the Logto user ID | |
|
||||
| Leave data unassigned until claimed | Nullable userId, user claims data on first login | |
|
||||
|
||||
**User's choice:** Migration assigns all rows to user ID 1 (auto-selected)
|
||||
**Notes:** Single existing user, simplest approach.
|
||||
|
||||
---
|
||||
|
||||
## Category Uniqueness
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Composite unique (userId, name) | Each user has independent category namespace | ✓ |
|
||||
| Global unique (current) | All users share one category namespace | |
|
||||
|
||||
**User's choice:** Composite unique (userId, name) (auto-selected)
|
||||
**Notes:** Required by MULTI-03.
|
||||
|
||||
---
|
||||
|
||||
## Settings Scope
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Composite PK (userId, key) | Each user has own settings | ✓ |
|
||||
| Separate user_settings table | New table for per-user settings | |
|
||||
|
||||
**User's choice:** Composite PK (userId, key) (auto-selected)
|
||||
**Notes:** Required by MULTI-06. Minimal schema change.
|
||||
|
||||
---
|
||||
|
||||
## API Key Ownership
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| userId column on apiKeys | API keys belong to creating user, middleware resolves user scope | ✓ |
|
||||
| Shared API keys (no user scope) | API keys grant access to all data | |
|
||||
|
||||
**User's choice:** userId column on apiKeys (auto-selected)
|
||||
**Notes:** Required by MULTI-05 for MCP tool scoping.
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- Migration SQL approach
|
||||
- userId scoping helper vs inline where clauses
|
||||
- Default category creation strategy
|
||||
- Thread resolution cross-user checks
|
||||
- Service change ordering
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
Reference in New Issue
Block a user