ShipVeryFastShipVeryFast
Documentation

Add a feature (vertical slice)

Features in ShipVeryFast follow one shape: a typed model, a table, a validated API route behind the session gate, and a page. Learn it once and every feature looks the same. We will add a simple "notes" feature.

Define the model

Add a Zod schema in models/ so the shape is typed everywhere.

models/note.ts
import { z } from "zod";

export const noteSchema = z.object({
  id: z.string().uuid(),
  user_id: z.string().uuid(),
  body: z.string().min(1).max(2000),
  created_at: z.string(),
});

export type Note = z.infer<typeof noteSchema>;

Add the table

Add a migration with RLS so a user only sees their own notes, the same pattern as every other table.

supabase/migrations/0002_notes.sql
create table public.notes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references public.users(id),
  body text not null,
  created_at timestamptz default now()
);
alter table public.notes enable row level security;
create policy "own notes" on public.notes
  for all using (auth.uid() = user_id);

Write the API route

Validate the body with Zod, check the session, then write. This is the house pattern for every mutating route.

app/api/notes/route.ts
export async function POST(req: Request) {
  const session = await getServerSession(authOptions);
  if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });

  const parsed = noteSchema.pick({ body: true }).safeParse(await req.json());
  if (!parsed.success) return Response.json({ error: "Invalid" }, { status: 400 });

  // insert with the anon client, RLS scopes it to this user
  return Response.json({ ok: true });
}

Build the page

Add app/notes/page.tsx, fetch on the server, and render. Protection is already handled by middleware.ts.

Let an AI scaffold it

This pattern is in CLAUDE.md and AGENTS.md, so an agent can generate the whole slice from a one-line description. See Get help from AI.