Common patterns
"How do I add a new X?" recipes for the most common changes.
Add a new field to Storefront theme
Touches 4 files in slabtrack-storefront. Skipping any = silent partial save.
- Schema —
prisma/schema.prisma: add the field to theStorefrontmodel with a default - Resolver —
src/lib/storefront-theme.ts: add toStorefrontThemeinterface,BASE_DEFAULTS,resolveStorefrontTheme(), and to each preset that overrides it - API validator —
src/app/api/storefronts/[slug]/theme/route.ts: add validation +data[field] = ...assignment - Studio form —
src/app/dashboard/[slug]/theme/ThemeStudio.tsx: add toinitial+stateshape, render a form control, include in the Save state spread - (if visual) Update
src/app/[slug]/page.tsxto readtheme.newFieldand render conditionally
Add a new Stripe webhook event handler
- Open
src/app/api/stripe/webhook/route.tsin the relevant repo - Add a
case 'event.type':in the switch - Cast:
const obj = event.data.object as Stripe.RelevantType - Do work. Use
prisma.$transactionif multiple rows update together. - Add the event name to the Stripe dashboard webhook endpoint config (manual step in Stripe UI)
- Update
/docs/payments/stripe-setup.htmlevents list
Add a new channel
- Registry —
slabtrack/backend/services/channels/registry.js: add an entry with slug, name, accent, exclusive flag, etc. - Adapter —
slabtrack/backend/services/channels/{slug}.js: implementengage(card, ctx)+disengage(card, ctx) - Strategist scoring —
backend/services/channel-scoring/index.js: add scoring logic for the new channel (when does the engine recommend this channel for a card?) - Allocator —
backend/services/strategist/allocator.js: add a bucket for the new channel + its commit handling - UI: add to Strategist's quadrant view + Command Bridge's channel grid
- Webhook: if it's a satellite, point its inbound webhook at SlabTrack's ecosystem-transfer endpoint
Add a new game type to ToT
- Schema —
thisorthat/prisma/schema.prisma: extendGameTypeenum, add a new config table (FlipConfig/PullConfigprecedent) - Game logic —
src/lib/games/{name}.ts: pure function that takes a seed + listing + buyer state and returns an outcome - Commit endpoint —
src/app/api/play/{name}/route.ts: handles commit + reveal, follows the Pull/Flip provably-fair pattern (seed → hash → anchor → reveal) - Stripe: payment intent with manual capture, settled via webhook on outcome
- UI: game-specific commit page + result page + buyer onboarding
- Listing creation: SlabTrack's allocator + commit endpoints need to know about the new game type
Add a new Prisma model
- Edit
prisma/schema.prisma - Run
npx prisma generatelocally to update the client types - Test type-check passes (
npx tsc --noEmit -p .) - Commit. Railway's start script runs
prisma db pushon deploy. - Don't forget: update Prisma indexes for foreign keys + frequent query patterns
Add idempotency to a Stripe call
// Pattern: { action }-{ stable-id }
const idempotencyKey = `refund-${order.id}`;
await stripe.refunds.create(
{ payment_intent: order.paymentIntentId, ... },
{ stripeAccount: sf.stripeAccountId, idempotencyKey }
);
Stripe will return the same Refund/Session/PaymentIntent object for repeat calls with the same key (within 24h). The retry-safe semantic.
Add a new API endpoint
- Pick the right repo + route file
- For SlabTrack (Express):
backend/routes/{topic}.routes.js— define the route, mount inserver.jsif new file - For ToT/Storefront (Next 16):
src/app/api/{path}/route.ts— exportGET/POST/ etc. - Auth: pull from cookie via
verifyToken(token)(Next) orauthenticatemiddleware (Express) - Owner check: if mutating a shop/order/etc., verify
req.user.id === resource.ownerUserId - Return shape:
{ success: true, ...data }or{ success: false, error: '...' }
Add a Cmd+K command to a palette
- For ToT: edit
src/components/CommandPalette.tsx, add to theactionsuseMemo array - For Storefront: edit
src/components/ShopCommandPalette.tsx - Each action needs:
id,label,section(nav/filter/recent),icon,run() - The
run()typically callsrouter.push(...)with a URL the target page parses on mount
Update this manual
- Identify which manual the change belongs to (operator/seller/buyer/collector/ai)
- Find the existing page that covers the area, OR create a new one
- If new page: copy boilerplate from a sibling page, swap the title, replace content
- Update
src/components/sidebar.js(in/docs/assets/) NAV constant if adding a new entry - Add a "Recent decisions" entry to
/docs/ai/recent-decisions.htmlif architecturally significant