--- phase: 32-setup-sharing-system plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/db/schema.ts - src/server/services/setup.service.ts - src/server/services/discovery.service.ts - src/server/services/profile.service.ts - src/server/routes/setups.ts - src/shared/schemas.ts - src/shared/types.ts - src/client/hooks/useSetups.ts - src/client/components/SetupCard.tsx - src/client/components/SetupsView.tsx - src/client/routes/setups/$setupId.tsx - tests/services/setup.service.test.ts - tests/services/discovery.service.test.ts - tests/services/profile.service.test.ts autonomous: true requirements: - TBD must_haves: truths: - "setups table has visibility text column with values private/link/public instead of isPublic boolean" - "shares table exists with id, setupId, token, permission, expiresAt, userId, createdAt, revokedAt columns" - "Discovery feed returns only setups with visibility='public'" - "Public profile returns only setups with visibility='public'" - "All existing isPublic=true setups migrated to visibility='public'" - "All existing isPublic=false setups migrated to visibility='private'" artifacts: - path: "src/db/schema.ts" provides: "Updated setups table with visibility column, new shares table" contains: "visibility.*text.*notNull.*default.*private" - path: "drizzle/" provides: "Migration SQL for visibility column and shares table" key_links: - from: "src/server/services/discovery.service.ts" to: "src/db/schema.ts" via: "visibility column filter" pattern: "visibility.*public" - from: "src/server/services/profile.service.ts" to: "src/db/schema.ts" via: "visibility column filter" pattern: "visibility.*public" --- Migrate the setups visibility model from boolean isPublic to three-tier visibility (private/link/public), add shares table to schema, and update all services, routes, schemas, and client code that reference isPublic. Purpose: This is the foundational schema change required by all other plans. Every service, route, and component that references isPublic must be updated atomically to prevent broken queries. Output: Updated schema with visibility column and shares table, migrated data, updated services/routes/schemas/hooks/components. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/32-setup-sharing-system/32-CONTEXT.md @.planning/phases/32-setup-sharing-system/32-RESEARCH.md From src/db/schema.ts (current setups table, line 118-127): ```typescript export const setups = pgTable("setups", { id: serial("id").primaryKey(), name: text("name").notNull(), userId: integer("user_id").notNull().references(() => users.id), isPublic: boolean("is_public").notNull().default(false), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); ``` From src/shared/schemas.ts (setup schemas, lines 86-98): ```typescript export const createSetupSchema = z.object({ name: z.string().min(1, "Setup name is required"), isPublic: z.boolean().optional().default(false), }); export const updateSetupSchema = z.object({ name: z.string().min(1, "Setup name is required"), isPublic: z.boolean().optional(), }); ``` From src/server/services/setup.service.ts (createSetup uses isPublic): ```typescript export async function createSetup(db: Db, userId: number, data: CreateSetup) { const [row] = await db.insert(setups) .values({ name: data.name, userId, isPublic: data.isPublic ?? false }) .returning(); return row; } ``` From src/server/services/discovery.service.ts (line 53): ```typescript .where(eq(setups.isPublic, true)) ``` From src/server/services/profile.service.ts (lines 82, 91): ```typescript .where(and(eq(setups.userId, userId), eq(setups.isPublic, true))); // and: .where(and(eq(setups.id, setupId), eq(setups.isPublic, true))); ``` Task 1: Update schema — add visibility column, shares table, generate migration src/db/schema.ts src/db/schema.ts 1. In `src/db/schema.ts`, modify the `setups` table: - Remove `isPublic: boolean("is_public").notNull().default(false)` (per D-02) - Add `visibility: text("visibility").notNull().default("private")` (per D-01, D-02) 2. Add new `shares` table after `setupItems` (per D-10, D-11, D-12): ```typescript export const shares = pgTable("shares", { id: serial("id").primaryKey(), setupId: integer("setup_id") .notNull() .references(() => setups.id, { onDelete: "cascade" }), token: text("token").notNull().unique(), permission: text("permission").notNull().default("read"), expiresAt: timestamp("expires_at"), userId: integer("user_id").references(() => users.id), createdAt: timestamp("created_at").defaultNow().notNull(), revokedAt: timestamp("revoked_at"), }); ``` 3. Run `bun run db:generate` to generate the Drizzle migration. 4. The generated migration will likely create a new column and drop the old one. Edit the migration SQL to include data migration: - After `ALTER TABLE setups ADD COLUMN visibility text NOT NULL DEFAULT 'private'`, add: - `UPDATE setups SET visibility = 'public' WHERE is_public = true;` - Then the `ALTER TABLE setups DROP COLUMN is_public` statement. 5. Run `bun run db:push` to apply the migration. grep -q "visibility" src/db/schema.ts && grep -q "shares" src/db/schema.ts && echo "PASS" || echo "FAIL" - `src/db/schema.ts` contains `visibility: text("visibility").notNull().default("private")` in setups table - `src/db/schema.ts` contains `shares` table with columns: id, setupId, token, permission, expiresAt, userId, createdAt, revokedAt - `src/db/schema.ts` does NOT contain `isPublic` or `is_public` - A new migration file exists in `drizzle/` directory - `bun run db:push` succeeds without error Schema updated, migration generated and applied, isPublic replaced with visibility, shares table created Task 2: Update all services, routes, schemas, and client code from isPublic to visibility src/server/services/setup.service.ts, src/server/services/discovery.service.ts, src/server/services/profile.service.ts, src/server/routes/setups.ts, src/shared/schemas.ts, src/shared/types.ts, src/client/hooks/useSetups.ts, src/client/components/SetupCard.tsx, src/client/components/SetupsView.tsx, src/client/routes/setups/$setupId.tsx src/server/services/setup.service.ts, src/server/services/discovery.service.ts, src/server/services/profile.service.ts, src/shared/schemas.ts, src/client/hooks/useSetups.ts, src/client/routes/setups/$setupId.tsx, src/client/components/SetupCard.tsx, src/client/components/SetupsView.tsx **Shared schemas (`src/shared/schemas.ts`):** - Replace `createSetupSchema`: change `isPublic: z.boolean().optional().default(false)` to `visibility: z.enum(["private", "link", "public"]).optional().default("private")` - Replace `updateSetupSchema`: change `isPublic: z.boolean().optional()` to `visibility: z.enum(["private", "link", "public"]).optional()` **Setup service (`src/server/services/setup.service.ts`):** - `createSetup`: change `isPublic: data.isPublic ?? false` to `visibility: data.visibility ?? "private"` (per D-01) - `getAllSetups`: change `isPublic: setups.isPublic` in select to `visibility: setups.visibility` - `updateSetup`: change `data.isPublic` handling to `data.visibility` — set `updateData.visibility = data.visibility` when defined **Discovery service (`src/server/services/discovery.service.ts`):** - `getPopularSetups`: change `.where(eq(setups.isPublic, true))` to `.where(eq(setups.visibility, "public"))` (per D-19) **Profile service (`src/server/services/profile.service.ts`):** - `getPublicProfile`: change `eq(setups.isPublic, true)` to `eq(setups.visibility, "public")` (per D-19) - `getPublicSetupWithItems`: change `eq(setups.isPublic, true)` to `eq(setups.visibility, "public")` (per D-19) **Setup routes (`src/server/routes/setups.ts`):** - No route changes needed — routes use service functions and Zod schemas **Client hooks (`src/client/hooks/useSetups.ts`):** - `useUpdateSetup` mutation body: replace any `isPublic` references with `visibility` - All query return types will auto-update via TypeScript inference **Client components:** - `SetupCard.tsx`: replace any `isPublic` references with `visibility` checks (e.g., `setup.visibility === "public"` instead of `setup.isPublic`) - `SetupsView.tsx`: replace any `isPublic` references with `visibility` - `setups/$setupId.tsx`: Replace the globe toggle button (lines 177-203) with a temporary visibility indicator. For now, just show the current visibility state as a read-only badge (the full share modal comes in Plan 03). Replace: - `onClick={() => updateSetup.mutate({ isPublic: !setup.isPublic })}` - With a static badge showing visibility icon per 32-UI-SPEC.md color table: - private: lock icon, gray-500/gray-50 - link: link icon, blue-600/blue-50 - public: globe icon, green-700/green-50 - This button will be upgraded to open the share modal in Plan 03. **Also check and update:** - `src/server/routes/account.ts` if it references isPublic - `src/db/dev-seed.ts` and `src/db/dev-seed-data.ts` — update seed data to use `visibility` instead of `isPublic` - `src/client/routes/__root.tsx` if it references isPublic - Any MCP tool definitions that reference isPublic grep -r "isPublic" src/ --include="*.ts" --include="*.tsx" | grep -v node_modules | grep -v ".gen.ts" | wc -l | xargs -I{} test {} -eq 0 && echo "PASS" || echo "FAIL: isPublic references remain" - Zero occurrences of `isPublic` or `is_public` in `src/` directory (excluding node_modules and generated files) - `src/shared/schemas.ts` contains `visibility: z.enum(["private", "link", "public"])` - `src/server/services/discovery.service.ts` contains `eq(setups.visibility, "public")` - `src/server/services/profile.service.ts` contains `eq(setups.visibility, "public")` (two occurrences) - `src/server/services/setup.service.ts` contains `visibility: data.visibility` - `src/client/routes/setups/$setupId.tsx` shows visibility badge with lock/link/globe icons - `bun run lint` passes - `bun test` passes (existing tests may need updating in Task 3) All isPublic references replaced with visibility across the full stack Task 3: Update existing tests for visibility column tests/services/setup.service.test.ts, tests/services/discovery.service.test.ts, tests/services/profile.service.test.ts, tests/routes/discovery.test.ts, tests/routes/profiles.test.ts tests/services/setup.service.test.ts, tests/services/discovery.service.test.ts, tests/services/profile.service.test.ts Update all existing tests that reference `isPublic` to use `visibility` instead: 1. **`tests/services/setup.service.test.ts`**: Replace `isPublic: true` with `visibility: "public"`, `isPublic: false` with `visibility: "private"` in all test fixtures and assertions. 2. **`tests/services/discovery.service.test.ts`**: Replace `isPublic: true` with `visibility: "public"` in setup creation for discovery feed tests. 3. **`tests/services/profile.service.test.ts`**: Replace `isPublic: true` with `visibility: "public"` in setup creation for public profile tests. 4. **`tests/routes/discovery.test.ts`**: Update route test fixtures. 5. **`tests/routes/profiles.test.ts`**: Update route test fixtures. 6. **`tests/helpers/db.ts`**: If createTestDb seeds any setup data with isPublic, update to visibility. Run `bun test` to verify all tests pass after changes. bun test - Zero occurrences of `isPublic` in `tests/` directory - `bun test` exits with code 0 (all tests pass) - Discovery feed tests verify `visibility: "public"` setups appear - Profile tests verify only `visibility: "public"` setups are returned All existing tests pass with the visibility column changes ## Trust Boundaries | Boundary | Description | |----------|-------------| | client->API | Visibility enum value from untrusted client input | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-32-01 | Tampering | updateSetup endpoint | mitigate | Zod enum validation ensures only "private"/"link"/"public" accepted — `z.enum(["private", "link", "public"])` at route entry | | T-32-02 | Information Disclosure | getAllSetups | accept | getAllSetups is already scoped to authenticated userId — no cross-user visibility leak | 1. `bun run lint` passes 2. `bun test` passes — all existing tests updated for visibility 3. No `isPublic` references remain in `src/` or `tests/` 4. Schema migration applied successfully - isPublic column fully replaced by visibility column across entire codebase - shares table exists in schema (ready for Plan 02) - Discovery feed shows only visibility='public' setups (identical behavior to before) - All existing tests pass with visibility column After completion, create `.planning/phases/32-setup-sharing-system/32-01-SUMMARY.md`