How I build with Supabase
I use Supabase as my backend for everything I build these days. Here's a peek into my workflow and how I build personal software with it.
Getting started
- I always use the same tools: Supabase, Next.js, server components, nuqs, React Query when necessary, etc. This helps keep context (instructions) consistent and building off each other.
- I always start project myself, using Supabase UI. More specifically, I init every project manually using
npx create-next-app -e with-supabase. I find this step is important as it reduces variability and ensures consistent project structure. - I use Claude Code mostly, with Cursor for the hand-rolled bits. More on AI below.
Supabase
- I use declarative schemas obsessively.
- I number migration files with prefixes to ensure order and avoid conflicts
- I always start with
00_extensions - I always start projects with a single migration file. I might work on a project locally for weeks, but once I'm ready to make it live, there's just a single migration file. I'll delete any existing migration files, run
supabase db reset, thensupabase db diff -f initto create the new init migration. I just learned about squashing migrations withsupabase migration squashtoday, but I haven't used it yet. - Everything starts locally, then I move to hosted when I'm basically at a v1.
- The Dashboard is essentially "read-only". I make a change in code, create a new migration,
supabase migration upand then verify that it works. I don't do any creating in the Dashboard.
Seeding data
- I don't do any seeding with SQL (via a seed.sql file), though that's an out-of-the-box solution for Supabase.
- Instead, I seed with Typescript. At any time, I can run
npm run seed:<local | prod>and all of my data will be torn down and recreated. All users, all tables, everything. - I have AI create and update these scripts so I can get a proper environment working quickly
Testing RLS Policies
- Like seeding, I test my RLS policies with Typescript
- I create a Vitest setup in a
/tests/directory and create one test file per table. - Every table has a seed file to create a row to test
- The tests use the Supabase secret key to create users, populate tables, simulate access control, and clean up after itself
- The flow looks something like: create user -> seed table -> signInAs(user) -> try CRUD actions (e.g., "Alice can't update Bob's profile", "Bob can update his own profile", "Anyone can select posts", etc)
- You still need to think carefully about who can access what, but this setup lets you test in English which is really quick
Env vars
- start with
.env.example - duplicate this, create a
.env.local - duplicate this, create a
.env.prod - small thing, but doing this reduces the likelihood of annoying issues with mis-configured env vars
UX
- always store state in the url
- every possible state should be shareable via a url
- inputs should get focus when they appear (e.g., when a modal/popover is opened)
Esckey should work everywhere
AI
- I write very little code by hand these days. On small codebases like these personal projects, I don't have issues with context and Claude Code is able to handle it exactly like I want it
- I try to not rely on AI whenever possible. Ex: I'll use components from the UI Library any time I can
- I haven't found obsessing over CLAUDE.md files to be super useful. Maybe I'm doing it wrong, but sometimes my instructions just get ignored. I always watch what the AI is doing and tell it to do it the way I like (store state in the url, for example, is very commonly ignored until that pattern is established in actual files)
- Existing patterns in the codebase are worth more than instructions