---
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