Claude Code Supabase MCP CLAUDE.md PostgreSQL TypeScript

Claude Code + Supabase: MCP Setup, CLAUDE.md Templates, and Safe Migration Workflows (2026)

The Prompt Shelf ·

Claude Code already knows PostgreSQL. Supabase gives it a live database to work with, a type system to stay honest against, and an MCP server that closes the feedback loop entirely. Together they turn “write me a migration” from a copy-paste exercise into something Claude Code can draft, apply, and verify without leaving your terminal.

This guide covers the complete setup: MCP server configuration, a production-ready CLAUDE.md template for Supabase/Next.js projects, safe migration workflows, RLS policy patterns, type generation, and Edge Functions. Every code block is copy-paste ready.

We also keep a collection of real-world CLAUDE.md and AGENTS.md rules if you want to see how other teams configure their AI assistants.

Why Claude Code + Supabase Works

Most database tools fall into two camps: a chat interface that can answer questions about your schema, or a migration runner that just executes what you hand it. The Supabase MCP server bridges them — Claude Code can inspect your actual schema, generate a migration, apply it, and then validate the result, all in a single session.

The specific combination works because:

Schema awareness is real. Through MCP, Claude Code can call list_tables, read your existing types, and understand your foreign key graph before writing a single line of SQL. It doesn’t hallucinate column names because it can look them up.

Type generation creates a tight feedback loop. After a migration, Claude Code can run supabase gen types typescript and immediately update the types that every other file in your project depends on. Schema changes propagate automatically.

Local dev stack is production-equivalent. The Supabase CLI runs a full Postgres instance locally. Every migration Claude Code drafts gets tested against real Postgres semantics, not a SQLite compatibility layer.

RLS is first-class. Row Level Security is the right way to secure a Supabase database. Claude Code can read, write, and reason about RLS policies — but only if you tell it the rules in CLAUDE.md.

Setting Up the Supabase MCP Server

Supabase ships a hosted MCP server at mcp.supabase.com. You can also point it at your local Supabase CLI instance for fully offline development.

First, generate a personal access token in your Supabase dashboard. Label it something like claude-code-dev.

Find your project ref in your project’s settings URL: https://supabase.com/dashboard/project/<project_ref>/settings/general.

Add this to your Claude Code MCP configuration (.claude/mcp.json or the global ~/.claude/mcp.json):

{
  "mcpServers": {
    "supabase": {
      "type": "http",
      "url": "https://mcp.supabase.com/mcp?project_ref=${SUPABASE_PROJECT_REF}",
      "headers": {
        "Authorization": "Bearer ${SUPABASE_ACCESS_TOKEN}"
      }
    }
  }
}

Set the environment variables in your shell profile or .env.local:

export SUPABASE_PROJECT_REF="abcdefghijklmnop"
export SUPABASE_ACCESS_TOKEN="sbp_..."

Never hardcode these values in mcp.json or CLAUDE.md. Always reference them as environment variables.

Option 2: Local Supabase CLI

If you’re running supabase start locally, the MCP server is already available at your local API URL:

{
  "mcpServers": {
    "supabase": {
      "type": "http",
      "url": "http://localhost:54321/mcp"
    }
  }
}

No authentication required for local development — the CLI handles it.

Scoping Access (Important for Security)

The hosted MCP server supports query parameters to restrict what Claude Code can do:

https://mcp.supabase.com/mcp?project_ref=abc123&read_only=true

Use read_only=true when you only need Claude Code to inspect your schema or read data. This is appropriate for most “help me understand this database” conversations.

Use features=database,development to restrict which tool groups are available. Available groups: database, debugging, development, edge-functions, docs, storage.

{
  "mcpServers": {
    "supabase": {
      "type": "http",
      "url": "https://mcp.supabase.com/mcp?project_ref=${SUPABASE_PROJECT_REF}&features=database,development",
      "headers": {
        "Authorization": "Bearer ${SUPABASE_ACCESS_TOKEN}"
      }
    }
  }
}

Rule: Never connect the MCP server to a production Supabase project. Use a dedicated development project. If you need to inspect production data, do it with read-only mode enabled — and even then, weigh the risk carefully.

Verifying the Connection

Once configured, ask Claude Code:

List all tables in my Supabase database

It should call the list_tables tool and return your schema. If it responds with general knowledge instead of actual table names, the MCP connection isn’t working — check your environment variables and restart Claude Code.

CLAUDE.md Template for Supabase Projects

This template is designed for a Next.js + Supabase + TypeScript project. Adapt the sections that don’t apply.

# Project: [Your App Name]

## Stack
- Next.js 15 (App Router)
- Supabase (PostgreSQL + Auth + Storage + Edge Functions)
- TypeScript (strict mode)
- Tailwind CSS

## Commands
- Dev server: `npm run dev`
- Build: `npm run build`
- Type check: `npx tsc --noEmit`
- Lint: `npm run lint`
- Tests: `npm test`
- Supabase local: `supabase start` / `supabase stop`
- Generate types: `supabase gen types typescript --local > src/types/database.types.ts`
- New migration: `supabase migration new <name>`
- Apply migrations: `supabase db push` (local) / `supabase db push --db-url $SUPABASE_DB_URL` (remote)
- Reset local DB: `supabase db reset`

## Supabase Client Usage
- Use the client from `src/lib/supabase/client.ts` for browser-side queries
- Use the client from `src/lib/supabase/server.ts` for Server Components and Route Handlers
- Never import the admin client (`createClient` with service role key) in client-side code
- Types: always use `Database` from `src/types/database.types.ts`. Never write raw string table names

## Architecture
- `src/app/` — Next.js App Router pages and layouts
- `src/components/` — Shared UI components
- `src/lib/supabase/` — Supabase client instances (browser, server, admin)
- `src/types/database.types.ts` — Auto-generated Supabase types (do not edit manually)
- `supabase/migrations/` — SQL migration files (auto-numbered)
- `supabase/functions/` — Edge Functions

## Database Conventions
- All tables use `id uuid DEFAULT gen_random_uuid() PRIMARY KEY`
- All tables have `created_at timestamptz DEFAULT now()` and `updated_at timestamptz DEFAULT now()`
- Foreign keys always include `ON DELETE` behavior (CASCADE or SET NULL, never RESTRICT without comment)
- Use snake_case for all table and column names
- Soft deletes via `deleted_at timestamptz` column (not hard deletes)

## Row Level Security Rules
- RLS is ENABLED on all tables. Never disable it.
- Every table must have at least one SELECT policy before data can be read
- `auth.uid()` = the currently authenticated user's UUID
- Standard ownership pattern: `(auth.uid() = user_id)`
- Service role bypasses RLS — use it only in Edge Functions that need elevated access
- When writing migrations that add new tables, always include the RLS enable statement AND at least the basic policies in the same migration file
- Pattern for user-owned data:
  ```sql
  ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;

  CREATE POLICY "Users can view own rows"
    ON table_name FOR SELECT
    USING (auth.uid() = user_id);

  CREATE POLICY "Users can insert own rows"
    ON table_name FOR INSERT
    WITH CHECK (auth.uid() = user_id);

  CREATE POLICY "Users can update own rows"
    ON table_name FOR UPDATE
    USING (auth.uid() = user_id);

  CREATE POLICY "Users can delete own rows"
    ON table_name FOR DELETE
    USING (auth.uid() = user_id);
  • Team/org-scoped data: see RLS patterns in docs/rls-patterns.md

Migration Rules

  • Generate migration files with supabase migration new <descriptive-name>
  • Migration file names: supabase auto-assigns timestamp prefix, then your name
  • Never edit existing migration files that have been applied to production
  • New columns must be nullable or have a DEFAULT — adding NOT NULL columns to existing tables requires a default value
  • Always test migrations locally with supabase db reset before pushing
  • Include rollback comments in complex migrations so humans can reverse manually if needed
  • When adding indexes: add them in a separate migration or use CREATE INDEX CONCURRENTLY to avoid locking

Type Generation

  • After any schema change (migration applied), run: supabase gen types typescript --local > src/types/database.types.ts
  • The Database type is the source of truth for table shapes
  • Helper types at src/types/helpers.ts:
    • Tables<T> — row type for table T
    • TablesInsert<T> — insert type for table T
    • TablesUpdate<T> — update type for table T

Edge Functions

  • Located in supabase/functions/<function-name>/index.ts
  • Use Deno runtime — no Node.js APIs
  • Import from jsr:@supabase/supabase-js (JSR registry, not npm)
  • Admin client in Edge Functions: use SUPABASE_SERVICE_ROLE_KEY from Deno.env
  • Deploy: supabase functions deploy <function-name>
  • Test locally: supabase functions serve <function-name>
  • Edge Functions can bypass RLS using the admin client — document why when you do this

Security Constraints

  • NEVER put service role keys, JWT secrets, or database passwords in CLAUDE.md or any tracked file
  • NEVER generate code that exposes the service role key to the browser
  • ANON key is safe for client-side use (RLS protects the data)
  • Database passwords belong in environment variables only
  • .env.local is gitignored — keep all secrets there
  • .claudeignore should include: .env*, *.key, supabase/.temp/

Keep this CLAUDE.md under 200 lines. If the RLS patterns section grows, extract it to `docs/rls-patterns.md` and reference it from CLAUDE.md — Claude Code will read it when relevant.

## Safe Migration Workflows

The biggest risk with Claude Code + Supabase is a migration that looks right but breaks something. Here's a workflow that catches problems before they reach production.

### The Standard Flow
  1. Claude Code inspects current schema via MCP
  2. Claude Code generates migration SQL
  3. You review the SQL (required step — do not skip)
  4. Run locally: supabase db reset (applies all migrations from scratch)
  5. Claude Code runs supabase gen types typescript
  6. Type errors surface any column/table mismatches immediately
  7. Push to staging: supabase db push —db-url $STAGING_DB_URL
  8. Verify staging, then push to production

Instruct Claude Code with this pattern when starting migration work:

Generate a migration to add a posts table with the following columns: [your column list]

Follow the project conventions from CLAUDE.md: uuid primary key, created_at/updated_at timestamps, snake_case names. Include RLS policies for user-owned data. After generating the migration file, run supabase db reset to verify it applies cleanly, then run type generation.


### Protecting Production

The single most effective safeguard: Claude Code should only have MCP access to your development project. Production gets no MCP connection. This means:

- Claude Code can draft and test migrations against dev
- You apply to production using a separate CI/CD pipeline or manual `supabase db push`
- Even if something goes wrong in the Claude Code session, production is untouched

If you're using Supabase's database branching feature (available on Pro plans), each branch has its own isolated database — perfect for letting Claude Code experiment freely.

### Reviewing Migrations Before Apply

Never let Claude Code apply a migration without reading it. When Claude Code generates a migration file, check:

1. **Destructive operations** — `DROP TABLE`, `DROP COLUMN`, `TRUNCATE` deserve extra scrutiny
2. **Missing RLS policies** — new tables without RLS are open by default
3. **NOT NULL without defaults** — this will fail on tables with existing rows
4. **Missing indexes** — foreign keys should generally have indexes
5. **Correct ON DELETE behavior** — CASCADE can delete a lot of data silently

Claude Code will generate good SQL most of the time. These are the edge cases where it occasionally misses project-specific constraints.

## Instructing Claude Code About RLS

RLS is where most AI-assisted Supabase development breaks down. The model knows what RLS is, but doesn't know your specific access model. CLAUDE.md is where you document it.

### Basic User-Owned Pattern

Already covered in the CLAUDE.md template above. Every row belongs to one user, identified by `user_id uuid REFERENCES auth.users(id)`.

### Team/Organization Pattern

For multi-tenant SaaS with team membership:

```sql
-- Add this to CLAUDE.md's RLS section, or extract to docs/rls-patterns.md

-- Table: team_members
-- Pattern: users can only access data that belongs to their team
-- team_id is resolved through the team_members junction table

CREATE POLICY "Team members can view team data"
  ON some_team_resource FOR SELECT
  USING (
    team_id IN (
      SELECT team_id FROM team_members
      WHERE user_id = auth.uid()
    )
  );

Tell Claude Code the pattern explicitly in CLAUDE.md:

## RLS: Team Access Pattern
- Resources scoped to a team use `team_id uuid REFERENCES teams(id)`
- Access check: `team_id IN (SELECT team_id FROM team_members WHERE user_id = auth.uid())`
- Admin access: `EXISTS (SELECT 1 FROM team_members WHERE team_id = table.team_id AND user_id = auth.uid() AND role = 'admin')`

Public Read, Authenticated Write

Common for content-heavy apps:

## RLS: Public Content Pattern
- Public posts: authenticated and anonymous users can SELECT
- Only the author can INSERT/UPDATE/DELETE
- Policy for SELECT: `USING (true)` -- allows all reads
- Policy for INSERT: `WITH CHECK (auth.uid() = author_id)`
- Policy for UPDATE/DELETE: `USING (auth.uid() = author_id)`

What to Put in CLAUDE.md vs What to Leave Out

Put in CLAUDE.md:

  • The access patterns your app uses (user-owned, team-scoped, public/private)
  • The column naming convention for ownership (user_id, author_id, owner_id)
  • Any tables that have non-obvious RLS logic
  • The rule that RLS must always be enabled

Leave out of CLAUDE.md:

  • Actual policy SQL for every table (link to docs/rls-patterns.md instead)
  • Data in the database
  • Any credentials

Type Generation Workflow

After every migration, types need to regenerate. Make this automatic by adding it to your migration instructions in CLAUDE.md:

## After Any Migration
Run these commands in sequence:
1. `supabase db reset` — verifies migration applies cleanly on fresh DB
2. `supabase gen types typescript --local > src/types/database.types.ts` — regenerates types
3. `npx tsc --noEmit` — catches any type errors the schema change introduced

When Claude Code follows this sequence, type errors become the test suite for your migration. If a column gets renamed in SQL, TypeScript will immediately show every call site that needs updating.

Helper Types

Add this to src/types/helpers.ts so Claude Code has convenient shorthand types:

import type { Database } from './database.types';

export type Tables<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Row'];

export type TablesInsert<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Insert'];

export type TablesUpdate<T extends keyof Database['public']['Tables']> =
  Database['public']['Tables'][T]['Update'];

export type Enums<T extends keyof Database['public']['Enums']> =
  Database['public']['Enums'][T];

Then in CLAUDE.md:

## Preferred Type Usage
- `Tables<'posts'>` instead of `Database['public']['Tables']['posts']['Row']`
- `TablesInsert<'posts'>` for insert operations
- `TablesUpdate<'posts'>` for update operations

Edge Functions Development

Claude Code handles Edge Functions well because they follow a predictable structure. The main thing to communicate in CLAUDE.md is the Deno/JSR distinction — it’s the most common mistake.

## Edge Functions
- Runtime: Deno (not Node.js)
- Package imports: use JSR (`jsr:`) or esm.sh, not npm
- Supabase client: `import { createClient } from 'jsr:@supabase/supabase-js@2'`
- Environment variables: `Deno.env.get('VAR_NAME')` (not process.env)
- CORS: include CORS headers for browser-callable functions
- Function URL pattern: `${SUPABASE_URL}/functions/v1/<function-name>`

A typical Edge Function that Claude Code handles reliably:

// supabase/functions/send-welcome-email/index.ts
import { createClient } from 'jsr:@supabase/supabase-js@2'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // Function logic here
  const { user_id } = await req.json()

  // ...

  return new Response(JSON.stringify({ success: true }), {
    headers: { ...corsHeaders, 'Content-Type': 'application/json' },
  })
})

Prompt Claude Code like this when creating Edge Functions:

Create an Edge Function in supabase/functions/send-welcome-email/index.ts that:
- Accepts a POST request with { user_id: string }
- Fetches the user's email from the profiles table using the admin client
- Sends a welcome email via Resend
- Returns { success: true } on success

Follow the Edge Function conventions in CLAUDE.md. Include CORS handling. Use SUPABASE_SERVICE_ROLE_KEY for the admin client, not the anon key.

AGENTS.md for Team Supabase Projects

If multiple engineers are using AI assistants on the same codebase, AGENTS.md at the repo root gives shared baseline instructions that any AI tool picks up.

# AGENTS.md

## Database Migrations
- All schema changes go through Supabase migrations in `supabase/migrations/`
- Never modify the database directly via dashboard in development — use migrations so all team members can reproduce the schema
- Migration naming: `<timestamp>_<verb>_<table>_<what>.sql` (supabase migration new handles the timestamp)
- After applying a migration locally: run `supabase gen types typescript --local > src/types/database.types.ts` and commit the updated types file along with the migration

## RLS Policy Requirements
- Every new table requires RLS enabled in the same migration that creates the table
- Do not create tables without at least a stub SELECT policy
- Use the access patterns documented in `docs/rls-patterns.md`

## TypeScript Types
- `src/types/database.types.ts` is auto-generated — never edit it manually
- Commit the regenerated types file every time a migration is applied

## Security
- Service role key must never appear in client-side code or git history
- If a secret is accidentally committed: rotate it immediately in the Supabase dashboard, then clean the git history

## Reviewing AI-Generated Migrations
Before applying any AI-generated migration:
1. Read the entire SQL file — not just the diff
2. Verify RLS policies are included for new tables
3. Verify destructive operations (DROP, TRUNCATE) are intentional
4. Run `supabase db reset` locally to confirm it applies without errors

Security: What NOT to Put in CLAUDE.md

This deserves its own section because it’s the mistake that turns a helpful setup into a security incident.

Never include in CLAUDE.md or any tracked file:

  • SUPABASE_SERVICE_ROLE_KEY — gives full database access bypassing RLS
  • SUPABASE_DB_PASSWORD — direct database access
  • SUPABASE_JWT_SECRET — allows forging auth tokens
  • Any personal access tokens for the Supabase dashboard
  • Connection strings that embed credentials

What’s safe to include:

  • SUPABASE_URL — the project URL is not a secret
  • SUPABASE_ANON_KEY — designed for public use, protected by RLS
  • Table names, column names, schema structure
  • RLS policy patterns (these are access control logic, not credentials)

Using .claudeignore

Create .claudeignore in your project root to prevent Claude Code from reading files that might contain secrets:

# .claudeignore
.env
.env.local
.env.*.local
.env.production
*.pem
*.key
supabase/.temp/

This prevents Claude Code from accidentally reading your environment files when doing file searches. It won’t prevent Claude Code from using environment variables that are already set in your shell — that’s intentional and fine.

Complete Workflow Example

Putting it all together: adding a comments feature to an existing posts app.

1. Start the session:

I need to add a comments system. Users can comment on posts. Each comment 
belongs to one user and one post. Comments can be soft-deleted.

Start by inspecting the current schema, then generate the migration.

Claude Code will call list_tables via MCP, see your posts and profiles tables, then generate:

-- supabase/migrations/20260618120000_add_comments_table.sql
CREATE TABLE comments (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  post_id uuid NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  content text NOT NULL,
  deleted_at timestamptz,
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now()
);

CREATE INDEX comments_post_id_idx ON comments(post_id);
CREATE INDEX comments_user_id_idx ON comments(user_id);

ALTER TABLE comments ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Anyone can view non-deleted comments"
  ON comments FOR SELECT
  USING (deleted_at IS NULL);

CREATE POLICY "Authenticated users can insert comments"
  ON comments FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can soft-delete own comments"
  ON comments FOR UPDATE
  USING (auth.uid() = user_id);

2. Review and apply:

Read the SQL. The policies look correct for this app. Apply:

supabase db reset
supabase gen types typescript --local > src/types/database.types.ts

3. Continue in Claude Code:

Types are updated. Now create:
- A server action in src/app/actions/comments.ts for adding and soft-deleting comments
- A CommentList component in src/components/CommentList.tsx
- A CommentForm component in src/components/CommentForm.tsx

Use the Tables helper type. Use the server Supabase client for the actions.

Because the CLAUDE.md establishes conventions and the types are fresh, Claude Code generates code that compiles immediately.

Common Issues and Fixes

MCP tools not appearing in Claude Code: Restart Claude Code after modifying mcp.json. Environment variables must be set before Claude Code starts — modifying them mid-session doesn’t take effect.

“Permission denied” errors from MCP: Your access token may have expired or been revoked. Generate a new one in the Supabase dashboard.

Type errors after migration: Regenerate types with supabase gen types typescript --local > src/types/database.types.ts. If you’re running a remote project, use --project-id $SUPABASE_PROJECT_REF instead of --local.

Claude Code generating Node.js code for Edge Functions: Reinforce the Deno rule in your prompt: “This is a Supabase Edge Function running on Deno. Use Deno.env, JSR imports, and no Node.js APIs.”

RLS blocking queries that should work: The most common cause is a missing SELECT policy on a table that has RLS enabled. Run the Supabase Security Advisor (available via MCP’s debugging tools) to surface these.

What the MCP Server Makes Possible

Without MCP, Claude Code treats your Supabase project as a black box. You describe the schema in CLAUDE.md, hope it’s accurate, and manually verify every generated query. With MCP:

  • Claude Code can inspect the actual schema before writing migrations
  • It can catch foreign key mismatches before you run anything
  • It can use the TypeScript type generation tool directly
  • It can check Edge Function deployment status
  • It can read service logs to debug problems

The tools the Supabase MCP server exposes — table listing, migration execution, type generation, Edge Function management — are exactly the tools Claude Code needs to work autonomously on a Supabase project without requiring you to shuttle information back and forth manually.

The CLAUDE.md template and AGENTS.md above give Claude Code the project-specific context (your conventions, your RLS patterns, your security constraints) that the MCP server can’t provide. Both pieces are necessary.


For more CLAUDE.md examples and prompt patterns for database-heavy projects, browse the rules collection on The Prompt Shelf.

Related Articles

Explore the collection

Browse all AI coding rules — CLAUDE.md, .cursorrules, AGENTS.md, and more.

Browse Rules