# Typed Supabase Clients: Generate Types From Your Local Schema and Keep Them in Sync

`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 silent `undefined`
- `insert()` requires the columns that are `not null` without 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):

```bash
supabase gen types typescript --local > src/database.types.ts
```

With [Tinbase](https://www.tinbase.dev/?utm_source=hashnode&utm_medium=blog&utm_campaign=typed-supabase-client-local) (no Docker; I work on it):

```bash
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:

```ts
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:

```ts
// 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:

```tsx
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:

```json
{
  "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:

```bash
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:

```bash
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?

