Files
GearBox/.planning/phases/16-multi-user-data-model/16-03-PLAN.md

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
16-multi-user-data-model 03 execute 3
16-01
16-02
src/server/routes/items.ts
src/server/routes/categories.ts
src/server/routes/threads.ts
src/server/routes/setups.ts
src/server/routes/settings.ts
src/server/routes/totals.ts
src/server/routes/auth.ts
src/server/routes/images.ts
src/server/mcp/index.ts
src/server/mcp/tools/items.ts
src/server/mcp/tools/categories.ts
src/server/mcp/tools/threads.ts
src/server/mcp/tools/setups.ts
src/server/mcp/resources/collection.ts
true
MULTI-02
MULTI-05
MULTI-06
truths artifacts key_links
Every route handler extracts userId from context and passes it to service functions
Settings routes use userId for per-user settings
MCP tools receive userId and pass it to service functions
MCP server is created with userId from the authenticated session
No route calls a service function without passing userId
path provides contains
src/server/routes/items.ts User-scoped item routes c.get("userId")
path provides contains
src/server/routes/settings.ts Per-user settings routes c.get("userId")
path provides contains
src/server/mcp/index.ts MCP server with userId threading createMcpServer(db, userId)
from to via pattern
src/server/routes/items.ts src/server/services/item.service.ts userId passed from context to service getAllItems.*db.*userId
from to via pattern
src/server/mcp/index.ts src/server/mcp/tools/items.ts userId passed to registerItemTools registerItemTools.*db.*userId
Wire userId from Hono context into all route handlers and MCP tool registrations, completing the multi-user data isolation chain.

Purpose: Routes are the HTTP boundary. They extract userId (set by requireAuth middleware in Plan 01) and pass it to services (updated in Plan 02). MCP tools are the programmatic boundary. Together they ensure every data operation is scoped to the authenticated user.

Output: All route files and MCP tools pass userId to service calls. Settings use per-user composite key.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/16-multi-user-data-model/16-CONTEXT.md @.planning/phases/16-multi-user-data-model/16-RESEARCH.md @.planning/phases/16-multi-user-data-model/16-01-SUMMARY.md @.planning/phases/16-multi-user-data-model/16-02-SUMMARY.md @src/server/routes/items.ts @src/server/routes/categories.ts @src/server/routes/threads.ts @src/server/routes/setups.ts @src/server/routes/settings.ts @src/server/routes/totals.ts @src/server/routes/auth.ts @src/server/routes/images.ts @src/server/mcp/index.ts @src/server/mcp/tools/items.ts @src/server/mcp/tools/categories.ts @src/server/mcp/tools/threads.ts @src/server/mcp/tools/setups.ts

Route pattern (what every handler must do):

app.get("/", async (c) => {
  const db = c.get("db");
  const userId = c.get("userId");
  const result = await serviceFunction(db, userId, ...);
  return c.json(result);
});

MCP pattern (what must change):

// Before: createMcpServer(db: Db)
// After:  createMcpServer(db: Db, userId: number)
// Before: registerItemTools(db)
// After:  registerItemTools(db, userId)

Settings pattern (composite key):

// Before: eq(settings.key, key)
// After:  and(eq(settings.userId, userId), eq(settings.key, key))
// Insert with onConflict must target [settings.userId, settings.key]
Task 1: Update all route handlers to extract and pass userId src/server/routes/items.ts, src/server/routes/categories.ts, src/server/routes/threads.ts, src/server/routes/setups.ts, src/server/routes/settings.ts, src/server/routes/totals.ts, src/server/routes/auth.ts, src/server/routes/images.ts src/server/routes/items.ts, src/server/routes/categories.ts, src/server/routes/threads.ts, src/server/routes/setups.ts, src/server/routes/settings.ts, src/server/routes/totals.ts, src/server/routes/auth.ts, src/server/routes/images.ts For EVERY route handler in EVERY route file, add `const userId = c.get("userId");` after the `const db = c.get("db");` line, then pass `userId` to every service function call.
**items.ts**: Extract userId, pass to getAllItems(db, userId), getItemById(db, userId, id), createItem(db, userId, data), updateItem(db, userId, id, data), deleteItem(db, userId, id).

**categories.ts**: Extract userId, pass to getAllCategories(db, userId), getCategoryById(db, userId, id), createCategory(db, userId, data), updateCategory(db, userId, id, data), deleteCategory(db, userId, id).

**threads.ts**: Extract userId, pass to all thread service calls including addCandidate, updateCandidate, removeCandidate, resolveThread.

**setups.ts**: Extract userId, pass to all setup service calls including syncSetupItems.

**totals.ts**: Extract userId, pass to getTotals(db, userId) or equivalent.

**settings.ts** per D-06: This route does inline DB queries (no service file). Update to:
- GET `/:key`: Add userId to the where clause: `and(eq(settings.userId, userId), eq(settings.key, key))`
- PUT `/:key`: Update the upsert to use composite conflict target: `.onConflictDoUpdate({ target: [settings.userId, settings.key], set: { value: body.value } })` and include userId in the insert values: `.values({ userId, key, value: body.value })`
- Import `and` from `drizzle-orm` and `settings` from schema

**auth.ts**: Extract userId, pass to createApiKey(db, userId, name), listApiKeys(db, userId), deleteApiKey(db, userId, id). Auth routes that don't need userId (login, me, setup) can skip it.

**images.ts**: This route handles image uploads which don't directly involve userId scoping on the images table (images are stored by filename, not in a user-scoped table). However, if the route calls any service that now requires userId, pass it. Read the file first to determine what changes are needed.

IMPORTANT: The `Env` type annotation on each Hono app may need updating to include `userId` in the Variables type:
```typescript
type Env = { Variables: { db?: any; userId?: number } };
```
for f in src/server/routes/items.ts src/server/routes/categories.ts src/server/routes/threads.ts src/server/routes/setups.ts src/server/routes/settings.ts src/server/routes/totals.ts src/server/routes/auth.ts; do echo "$f: $(grep -c 'c.get("userId")' $f)"; done - Every route handler in items.ts, categories.ts, threads.ts, setups.ts, totals.ts, auth.ts contains `c.get("userId")` - Every service function call includes userId as the second argument - settings.ts uses `and(eq(settings.userId, userId), eq(settings.key, key))` for reads - settings.ts upsert targets `[settings.userId, settings.key]` for composite conflict - settings.ts insert includes userId in values - Env type includes `userId` in Variables - No service call is missing the userId parameter All route handlers extract userId from context and pass to every service call. Settings routes use composite key. Task 2: Update MCP server and tool registrations with userId src/server/mcp/index.ts, src/server/mcp/tools/items.ts, src/server/mcp/tools/categories.ts, src/server/mcp/tools/threads.ts, src/server/mcp/tools/setups.ts, src/server/mcp/resources/collection.ts src/server/mcp/index.ts, src/server/mcp/tools/items.ts, src/server/mcp/tools/categories.ts, src/server/mcp/tools/threads.ts, src/server/mcp/tools/setups.ts, src/server/mcp/resources/collection.ts Per D-13 and Research pitfall 5:
1. **Update `createMcpServer` signature** in `src/server/mcp/index.ts`:
Change from `createMcpServer(db: Db)` to `createMcpServer(db: Db, userId: number)`.
Pass userId to all `register*Tools` calls:
- `registerItemTools(db, userId)`
- `registerCategoryTools(db, userId)`
- `registerThreadTools(db, userId)`
- `registerSetupTools(db, userId)`
- `getCollectionSummary(db, userId)`
(registerImageTools has no db/userId dependency so leave unchanged)

2. **Update MCP auth middleware** to resolve userId:
The MCP auth middleware in `mcpRoutes.use("/*", ...)` currently calls `verifyAccessToken` and `verifyApiKey` which now return `{ userId } | null`. Store the userId and make it available to the POST handler.

Use the Hono context to pass userId, similar to the main API middleware:
```typescript
mcpRoutes.use("/*", async (c, next) => {
  const db = c.get("db") ?? prodDb;
  
  // Try Bearer token first (OAuth)
  const authHeader = c.req.header("Authorization");
  if (authHeader?.startsWith("Bearer ")) {
    const token = authHeader.slice(7);
    const result = await verifyAccessToken(db, token);
    if (result) {
      c.set("userId", result.userId);
      return next();
    }
    return c.json({ error: "invalid_token" }, 401);
  }

  // Try API key
  const apiKey = c.req.header("X-API-Key");
  if (apiKey) {
    const result = await verifyApiKey(db, apiKey);
    if (result) {
      c.set("userId", result.userId);
      return next();
    }
    return c.json({ error: "Invalid API key" }, 401);
  }
  // ... rest of auth handling
});
```

3. **Update MCP POST handler** to pass userId when creating MCP server:
In the `mcpRoutes.post("/", ...)` handler, extract userId from context and pass to createMcpServer:
```typescript
const userId = c.get("userId");
const server = createMcpServer(db, userId);
```

4. **Store userId alongside transport** in the session map per Research pitfall 5:
Change `transports` map type from `Map<string, Transport>` to `Map<string, { transport: Transport, userId: number }>`.
When reusing an existing session, extract userId from the stored session data (no need to recreate MCP server -- the session was already initialized with the correct userId).

5. **Update each tool registration file** to accept and use userId:
- `src/server/mcp/tools/items.ts`: `registerItemTools(db: Db, userId: number)` -- pass userId to all item service calls
- `src/server/mcp/tools/categories.ts`: `registerCategoryTools(db: Db, userId: number)` -- pass userId to all category service calls
- `src/server/mcp/tools/threads.ts`: `registerThreadTools(db: Db, userId: number)` -- pass userId to all thread service calls
- `src/server/mcp/tools/setups.ts`: `registerSetupTools(db: Db, userId: number)` -- pass userId to all setup service calls

6. **Update `getCollectionSummary`** in `src/server/mcp/resources/collection.ts`:
Add userId parameter, scope the summary queries to the user's data only.
grep -c "userId: number" src/server/mcp/index.ts && grep "createMcpServer(db, userId)" src/server/mcp/index.ts | wc -l && grep -c "userId: number" src/server/mcp/tools/items.ts && grep -c "userId: number" src/server/mcp/tools/categories.ts && grep -c "userId: number" src/server/mcp/tools/threads.ts && grep -c "userId: number" src/server/mcp/tools/setups.ts && grep -c "c.set(\"userId\"" src/server/mcp/index.ts - `createMcpServer` accepts `(db: Db, userId: number)` signature - MCP auth middleware sets `c.set("userId", result.userId)` for both API key and Bearer token auth - MCP POST handler passes userId to `createMcpServer(db, userId)` - Transport map stores userId alongside transport - All 4 tool registration functions accept `(db: Db, userId: number)` - All tool handlers pass userId to service function calls - `getCollectionSummary` accepts and uses userId - No MCP tool calls a service function without userId MCP server creation receives userId, all tool registrations pass userId to service calls, MCP auth middleware resolves userId from API key or Bearer token After all tasks complete: 1. `grep -r 'c.get("userId")' src/server/routes/ | wc -l` shows userId extraction in all route files 2. `grep -r 'c.get("userId")' src/server/mcp/ | wc -l` shows userId in MCP middleware 3. `grep "createMcpServer(db, userId)" src/server/mcp/index.ts` confirms MCP userId threading 4. No service call anywhere in routes/ or mcp/ is missing the userId argument

<success_criteria>

  • All route handlers extract userId from context and pass to services
  • Settings routes use composite PK for per-user settings
  • MCP server creation includes userId
  • MCP tool registrations pass userId to all service calls
  • MCP auth middleware resolves userId from API key and Bearer token
  • Complete chain: middleware sets userId -> routes/MCP extract it -> services filter by it </success_criteria>
After completion, create `.planning/phases/16-multi-user-data-model/16-03-SUMMARY.md`