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.