Files
GearBox/.planning/phases/32-setup-sharing-system/32-01-PLAN.md
Jean-Luc Makiola 81a654085d docs(32): create phase plans for setup sharing system
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>
2026-04-13 17:05:36 +02:00

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
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
true
TBD
truths artifacts key_links
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'
path provides contains
src/db/schema.ts Updated setups table with visibility column, new shares table visibility.*text.*notNull.*default.*private
path provides
drizzle/ Migration SQL for visibility column and shares table
from to via pattern
src/server/services/discovery.service.ts src/db/schema.ts visibility column filter visibility.*public
from to via pattern
src/server/services/profile.service.ts src/db/schema.ts visibility column filter 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.

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

From 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)));
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)
  1. Add new shares table after setupItems (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"),
});
  1. Run bun run db:generate to generate the Drizzle migration.

  2. 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.
  3. 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" <acceptance_criteria>

    • 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 </acceptance_criteria> 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" <acceptance_criteria>
    • 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) </acceptance_criteria> 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 <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>
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

<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>
After completion, create `.planning/phases/32-setup-sharing-system/32-01-SUMMARY.md`