4 plans in 3 waves: - Wave 1: Schema migration (isPublic→visibility) + shares table - Wave 2: Share link service + API routes - Wave 3: Share modal UI + shared setup viewer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 32-setup-sharing-system | 01 | execute | 1 |
|
true |
|
|
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.
<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/STATE.md @.planning/phases/32-setup-sharing-system/32-CONTEXT.md @.planning/phases/32-setup-sharing-system/32-RESEARCH.mdFrom src/db/schema.ts (current setups table, line 118-127):
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):
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):
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):
.where(eq(setups.isPublic, true))
From src/server/services/profile.service.ts (lines 82, 91):
.where(and(eq(setups.userId, userId), eq(setups.isPublic, true)));
// and:
.where(and(eq(setups.id, setupId), eq(setups.isPublic, true)));
- Add new
sharestable aftersetupItems(per D-10, D-11, D-12):
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"),
});
-
Run
bun run db:generateto generate the Drizzle migration. -
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_publicstatement.
- After
-
Run
bun run db:pushto apply the migration. grep -q "visibility" src/db/schema.ts && grep -q "shares" src/db/schema.ts && echo "PASS" || echo "FAIL" <acceptance_criteria>src/db/schema.tscontainsvisibility: text("visibility").notNull().default("private")in setups tablesrc/db/schema.tscontainssharestable with columns: id, setupId, token, permission, expiresAt, userId, createdAt, revokedAtsrc/db/schema.tsdoes NOT containisPublicoris_public- A new migration file exists in
drizzle/directory bun run db:pushsucceeds without error </acceptance_criteria> Schema updated, migration generated and applied, isPublic replaced with visibility, shares table created
Setup service (src/server/services/setup.service.ts):
createSetup: changeisPublic: data.isPublic ?? falsetovisibility: data.visibility ?? "private"(per D-01)getAllSetups: changeisPublic: setups.isPublicin select tovisibility: setups.visibilityupdateSetup: changedata.isPublichandling todata.visibility— setupdateData.visibility = data.visibilitywhen 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: changeeq(setups.isPublic, true)toeq(setups.visibility, "public")(per D-19)getPublicSetupWithItems: changeeq(setups.isPublic, true)toeq(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):
useUpdateSetupmutation body: replace anyisPublicreferences withvisibility- All query return types will auto-update via TypeScript inference
Client components:
SetupCard.tsx: replace anyisPublicreferences withvisibilitychecks (e.g.,setup.visibility === "public"instead ofsetup.isPublic)SetupsView.tsx: replace anyisPublicreferences withvisibilitysetups/$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.tsif it references isPublicsrc/db/dev-seed.tsandsrc/db/dev-seed-data.ts— update seed data to usevisibilityinstead ofisPublicsrc/client/routes/__root.tsxif 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"
<acceptance_criteria>
- Zero occurrences of
isPublicoris_publicinsrc/directory (excluding node_modules and generated files) src/shared/schemas.tscontainsvisibility: z.enum(["private", "link", "public"])src/server/services/discovery.service.tscontainseq(setups.visibility, "public")src/server/services/profile.service.tscontainseq(setups.visibility, "public")(two occurrences)src/server/services/setup.service.tscontainsvisibility: data.visibilitysrc/client/routes/setups/$setupId.tsxshows visibility badge with lock/link/globe iconsbun run lintpassesbun testpasses (existing tests may need updating in Task 3) </acceptance_criteria> All isPublic references replaced with visibility across the full stack
- Zero occurrences of
-
tests/services/setup.service.test.ts: ReplaceisPublic: truewithvisibility: "public",isPublic: falsewithvisibility: "private"in all test fixtures and assertions. -
tests/services/discovery.service.test.ts: ReplaceisPublic: truewithvisibility: "public"in setup creation for discovery feed tests. -
tests/services/profile.service.test.ts: ReplaceisPublic: truewithvisibility: "public"in setup creation for public profile tests. -
tests/routes/discovery.test.ts: Update route test fixtures. -
tests/routes/profiles.test.ts: Update route test fixtures. -
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
<acceptance_criteria>
- 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
</acceptance_criteria>
All existing tests pass with the visibility column changes
<threat_model>
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 |
| </threat_model> |
<success_criteria>
- 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 </success_criteria>