Build This Now
Build This Now
speedy_devvkoen_salo
Blog/Handbook/Workflow/Claude Code With Supabase: Database, Auth, RLS

Claude Code With Supabase: Database, Auth, RLS

Set up Supabase in a Next.js project using Claude Code: migrations, row-level security policies, auth, and edge functions from a single terminal.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

Published May 3, 20269 min readHandbook hubWorkflow index

Problem: Getting Supabase wired up correctly takes hours. You need migrations tracked in version control, RLS policies on every table, auth middleware that reads the right cookie, and edge functions deployed before you can test anything. Do any one piece wrong and you get a working-looking app that either leaks data or throws a 401 on every authenticated call.

Quick Win: Wire the Supabase MCP server into Claude Code with one config block. Drop this into .claude/settings.json:

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

Restart Claude Code and it now has 32 tools pointing at your Supabase project. Tables, migrations, logs, edge functions, TypeScript types.

Why This Changed in Early 2026

Supabase became an official Claude connector in February 2026. Before that, Claude could write SQL all day but had no channel to actually run it against your project. The new MCP integration is native, not proxied through a third party. Claude calls apply_migration, execute_sql, get_advisors, deploy_edge_function directly. No wrapper layer.

The 32 tools are grouped into eight categories:

CategoryTools
Databaselist_tables, list_migrations, apply_migration, execute_sql, list_extensions
Debuggingget_logs, get_advisors
Developmentget_project_url, get_publishable_keys, generate_typescript_types
Edge Functionslist_edge_functions, get_edge_function, deploy_edge_function
Branchingcreate_branch, list_branches, merge_branch, reset_branch (paid plans)
Storagelist_storage_buckets, get_storage_config (disabled by default)
Accountlist_projects, create_project, list_organizations, get_cost
Docssearch_docs

Critical: the MCP server is for development and testing. Point it at a production database with write permissions and you're one mistyped prompt away from running an unreviewed migration on live data. Use read_only=true for production review, or scope the connection to a dev project with project_ref.

Three URL parameters let you tune what Claude can do:

ParameterExampleWhat it does
read_only=true...mcp?read_only=trueConnects as a read-only Postgres user. No writes.
project_ref=<ref>...mcp?project_ref=abcdefScopes the connection to one project only.
features=<groups>...mcp?features=database,docsEnables only the named tool groups.

Combine them freely: https://mcp.supabase.com/mcp?project_ref=${SUPABASE_PROJECT_REF}&read_only=true gives you read-only access on one specific project. That's the right shape for reviewing a staging database without any risk of accidental writes.

Install the Supabase Agent Skills Package

The MCP connection gives Claude access. The Agent Skills package tells Claude how to use it correctly.

Install it:

npx skills add supabase/agent-skills

Or from the Claude Code marketplace:

claude plugin marketplace add supabase/agent-skills

This drops a SKILL.md into your project. It's about 100 lines. It teaches Claude four things that MCP alone does not:

  • Verify Supabase behavior via search_docs before writing any implementation
  • Use app_metadata, not user_metadata, for authorization claims in RLS policies
  • Apply security_invoker = true to views (covered in detail below)
  • Run get_advisors before finalizing schema changes

The performance improvement is measurable. Claude Sonnet 4.6 goes from 58% task success on Supabase workflows with MCP only to 71% with the skill loaded. That 13-point gap comes from the specific footguns the skill prevents.

Running Migrations Through Claude Code

Schema changes go through the migration pipeline, not raw SQL in the Supabase dashboard. The pipeline keeps your codebase and your database in sync and gives you a history you can roll back.

Tell Claude what you need in plain English. Something like: "Create a profiles table linked to auth.users, with fields for display_name and avatar_url. Apply it as a migration." Claude drafts the SQL, shows it to you, then calls apply_migration after your review.

The five steps every schema change follows:

  1. Claude drafts the SQL
  2. You review it before it runs
  3. apply_migration executes and records the migration
  4. list_migrations confirms the migration was logged
  5. generate_typescript_types regenerates types to match the new schema

Step 5 matters. Every schema change that doesn't regenerate types will drift your TypeScript definitions away from reality. Claude runs it automatically if the skill is loaded.

Row-Level Security the Right Way

RLS is Postgres's built-in data permission system. You enable it on a table, write policies, and Postgres enforces those policies on every query. No application-layer check required. No way to accidentally skip it.

Enable RLS on a table:

alter table "profiles" enable row level security;

With RLS on and no policies written, zero rows are returned to anyone. You add policies to open up exactly what you want.

The four policy types, with real SQL:

SELECT policy (who can read):

create policy "User can see their own profile only."
on profiles
for select using ( (select auth.uid()) = user_id );

INSERT policy (who can create rows):

create policy "Users can create a profile."
on profiles for insert
to authenticated
with check ( (select auth.uid()) = user_id );

UPDATE policy (who can modify rows):

create policy "Users can update their own profile."
on profiles for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

DELETE policy (who can remove rows):

create policy "Users can delete a profile."
on profiles for delete
to authenticated
using ( (select auth.uid()) = user_id );

One gotcha: UPDATE policies require a SELECT policy to exist on the same table. Without SELECT, Postgres can't check what the user is trying to update. Claude knows this when the skill is loaded. Without the skill, it will sometimes write the UPDATE policy alone and leave the bug for you to find.

For team-based access, use app_metadata in your JWT claims:

create policy "User is in team"
on my_table
to authenticated
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));

The app_metadata vs user_metadata distinction matters for security. Data in user_metadata can be written by the user from the client. Data in app_metadata is server-set and the user cannot modify it. If you put authorization data in user_metadata, a user can add themselves to any team. That's a privilege escalation bug. Always use app_metadata for any data that drives an RLS policy.

Views and the security_invoker Gotcha

Database views bypass RLS by default. A view reads data as its definer's permissions, not the querying user's permissions. You can have perfect RLS on every underlying table and still leak all of it through an unprotected view.

The fix is one line:

create view public.reports_view
with (security_invoker = true)
as
select id, title, created_at
from public.reports;

security_invoker = true makes the view execute as the user who calls it, not the user who created it. The underlying table's RLS policies apply normally. Without it, every row is visible to anyone who can query the view.

This pattern is absent from most guides. The Supabase Agent Skills package prompts Claude to apply it automatically to every view it creates.

Setting Up Auth in Next.js

Supabase Auth ships two relevant methods: getUser() and getSession(). They are not interchangeable.

getSession() reads from local storage and is fast. It does not verify the token with the server. Use it for client-side display only, never for access control decisions.

getUser() makes a server request and validates the JWT. Use it for route protection, API authorization, and any check where correctness matters.

Server-side auth in Next.js uses the @supabase/ssr package. Tell Claude to wire it up and it will create the client factory and the middleware in the right shape:

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  if (!user && !request.nextUrl.pathname.startsWith('/login')) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

For OAuth, prompt Claude to add the provider in the Supabase dashboard and generate the callback route. For email+password with OTP, ask for the full flow: sign up, confirm email, sign in, sign out. Claude generates each piece and threads them together.

You also need a server-side Supabase client for Server Components and Route Handlers that don't go through middleware. Tell Claude to create a createClient utility using @supabase/ssr:

// utils/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Called from a Server Component. Middleware handles session refresh.
          }
        },
      },
    }
  )
}

Any Server Component or Route Handler can then call const supabase = await createClient() and get a typed client that reads the session from the request cookies.

Edge Functions

Edge functions run on Supabase's Deno-based infrastructure, not on Vercel or your server. Use them when you need a trusted server context: Stripe webhooks, sending emails with a secret key, operations that should never run on the client.

Claude can deploy an edge function directly through the MCP:

// supabase/functions/send-welcome/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

serve(async (req) => {
  const { email } = await req.json()
  
  // Send welcome email with your secret key
  const res = await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'welcome@yourapp.com',
      to: email,
      subject: 'Welcome',
      text: 'Thanks for signing up.',
    }),
  })

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

Tell Claude: "Deploy this as a Supabase edge function called send-welcome." It calls deploy_edge_function via MCP and the function is live. get_logs pulls the invocation logs back into the terminal so you can verify.

When to use edge functions over Next.js API routes: when you need a secret that cannot be in the Next.js bundle, when you need the closest possible execution to Supabase's Postgres, or when the logic is shared across multiple apps.

The Full Loop

The complete workflow from new feature to deployed table:

  1. Describe the schema: tell Claude what tables, columns, and relationships you need
  2. Review the SQL: Claude shows you the migration before running it
  3. Apply the migration: apply_migration runs it and records it
  4. Add RLS policies: describe who can read and write each table
  5. Verify the types: generate_typescript_types syncs your TypeScript
  6. Wire auth: tell Claude to add route protection for any page that needs it
  7. Deploy edge functions: for anything that needs a trusted server context

Every step runs from the same terminal session. No dashboard tabs, no copy-pasting SQL, no hunting for where to click.

Doing this manually teaches you how Supabase actually works. The MCP connection makes each step faster, not invisible. Once you've run the loop a few times and the patterns feel solid, Build This Now's db-architect and backend agents handle it automatically: RLS on every table from the first migration, correct auth patterns, TypeScript types synced on every schema change. The manual path and the automated path reach the same place. One just takes longer.

Continue in Workflow

  • Claude Code Best Practices
    Five habits separate engineers who ship with Claude Code: PRDs, modular CLAUDE.md rules, custom slash commands, /clear resets, and a system-evolution mindset.
  • Claude Code Auto Mode
    A second Sonnet model reviews every Claude Code tool call before it fires. What auto mode blocks, what it allows, and the allow rules it drops in your settings.
  • Claude Code Channels
    Plug Claude Code into Telegram, Discord, or iMessage with plugin MCP servers. Setup walkthroughs and the async mobile workflows that make it worth wiring up.
  • Building a Next.js App With Claude Code
    How to use Claude Code to build a full Next.js 16 app — from project setup through App Router, Server Components, and deployment.
  • Claude Code Pricing: What You'll Actually Pay
    Claude Code is free to install. What you pay depends on your plan. A plain-English breakdown of every tier, real usage costs, and which plan fits your workflow.
  • Adding Stripe Payments With Claude Code
    Wire up Stripe Checkout, webhooks, and the customer portal in a Next.js app using Claude Code. From first prompt to live payment in one session.

More from Handbook

  • Agent Fundamentals
    Five ways to build specialist agents in Claude Code: Task sub-agents, .claude/agents YAML, custom slash commands, CLAUDE.md personas, and perspective prompts.
  • Agent Harness Engineering
    The harness is every layer around your AI agent except the model itself. Learn the five control levers, the constraint paradox, and why harness design determines agent performance more than the model does.
  • Agent Patterns
    Orchestrator, fan-out, validation chain, specialist routing, progressive refinement, and watchdog. Six orchestration shapes to wire Claude Code sub-agents with.
  • Agent Teams Best Practices
    Battle-tested patterns for Claude Code Agent Teams. Context-rich spawn prompts, right-sized tasks, file ownership, delegate mode, and v2.1.33-v2.1.45 fixes.

Stop configuring. Start building.

SaaS builder templates with AI orchestration.

On this page

Why This Changed in Early 2026
Install the Supabase Agent Skills Package
Running Migrations Through Claude Code
Row-Level Security the Right Way
Views and the security_invoker Gotcha
Setting Up Auth in Next.js
Edge Functions
The Full Loop

Stop configuring. Start building.

SaaS builder templates with AI orchestration.