Typed Supabase Clients: Generate Types From Your Local Schema and Keep Them in Sync
Turn renamed columns and missing fields into compile errors, and make CI fail when types drift from your migrations

supabase-js works without types. You can call .from('notes').select('*') and get data back. But without a generated Database type, every row is loosely typed: a renamed column, a typo in a filter or a missing required field only shows up when the app runs.
Generating types from your local schema fixes that, and it costs one command. This guide covers how to generate them, use them, and keep them in sync with your migrations so they never drift.
What does a generated Database type give you?
When you pass the type to createClient<Database>(), the client knows every table, column and type in your schema:
select('titel')is a compile error instead of a silentundefinedinsert()requires the columns that arenot nullwithout a default- Query results are typed, so components get real types instead of
any - Renaming a column in a migration breaks the build exactly where the old name is used
How do I generate the types?
Generate them from a local database that has all your migrations applied, not from production. That way the types match the code on your branch.
With the Supabase CLI (Docker):
supabase gen types typescript --local > src/database.types.ts
With Tinbase (no Docker; I work on it):
npx tinbase gen types typescript > src/database.types.ts
Both read the schema created by your supabase/migrations and print a TypeScript Database type.
How do I use them?
Pass the type once, where you create the client:
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';
export const supabase = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
Every query is now checked against your schema:
// Typed result: { id: string; body: string }[] | null
const { data } = await supabase.from('notes').select('id, body');
// Compile error if 'body' is required and missing
await supabase.from('notes').insert({ body: 'hello' });
To type a component prop from a table row, pull it from the generated type:
type Note = Database['public']['Tables']['notes']['Row'];
function NoteCard({ note }: { note: Note }) {
return <p>{note.body}</p>;
}
The Supabase CLI output also includes helper types such as Tables<'notes'> and TablesInsert<'notes'>. If your generator doesn't emit them, the Database['public']['Tables'][...] path above always works.
How do I keep types in sync with migrations?
Types drift when someone changes a migration and forgets to regenerate. Three habits prevent it:
1. Regenerate right after every migration. Add a script so it's one command:
{
"scripts": {
"db:types": "tinbase gen types typescript > src/database.types.ts"
}
}
2. Commit the generated file. Reviewers then see schema changes as type changes in the same pull request.
3. Fail CI when types are stale. Regenerate in CI and check for a diff:
npx tinbase start &
sleep 5
npm run db:types
git diff --exit-code src/database.types.ts
If someone changed a migration without regenerating, git diff --exit-code fails the build and shows exactly what changed.
What about changes made outside migrations?
If you change the schema directly, for example in a dashboard, the types and the migrations both fall behind. Tinbase can write those changes out as a migration first:
npx tinbase db diff -f add_notes_pinned
Then regenerate the types as usual. The order is always schema change, then migration file, then types.
What can go wrong?
| Symptom | Likely cause |
|---|---|
| A new column isn't in the types | Types generated before the migration ran. Restart the local backend, then regenerate. |
| Types don't match production | Generated from a different branch or a database that's missing migrations. |
Everything is any |
createClient was called without <Database>. |
| Build breaks after a teammate's change | Working as intended: the types caught a schema change. Update the code that used the old shape. |
Wrapping up
Typed Supabase clients turn schema mistakes into compile errors. Generate the types from your local, fully migrated database, pass them to createClient<Database>(), commit the file, and let CI fail when it's stale.
Do you generate Supabase types in CI today, or regenerate them by hand when something breaks?
Comments
Join the discussionNo comments yet. Be the first to comment.
Docker-Free Supabase Development
Practical guides for local Supabase development without Docker: migrations, testing, types, edge functions and CI.