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.
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。
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:
| Category | Tools |
|---|---|
| Database | list_tables, list_migrations, apply_migration, execute_sql, list_extensions |
| Debugging | get_logs, get_advisors |
| Development | get_project_url, get_publishable_keys, generate_typescript_types |
| Edge Functions | list_edge_functions, get_edge_function, deploy_edge_function |
| Branching | create_branch, list_branches, merge_branch, reset_branch (paid plans) |
| Storage | list_storage_buckets, get_storage_config (disabled by default) |
| Account | list_projects, create_project, list_organizations, get_cost |
| Docs | search_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:
| Parameter | Example | What it does |
|---|---|---|
read_only=true | ...mcp?read_only=true | Connects as a read-only Postgres user. No writes. |
project_ref=<ref> | ...mcp?project_ref=abcdef | Scopes the connection to one project only. |
features=<groups> | ...mcp?features=database,docs | Enables 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-skillsOr from the Claude Code marketplace:
claude plugin marketplace add supabase/agent-skillsThis 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_docsbefore writing any implementation - Use
app_metadata, notuser_metadata, for authorization claims in RLS policies - Apply
security_invoker = trueto views (covered in detail below) - Run
get_advisorsbefore 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:
- Claude drafts the SQL
- You review it before it runs
apply_migrationexecutes and records the migrationlist_migrationsconfirms the migration was loggedgenerate_typescript_typesregenerates 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:
- Describe the schema: tell Claude what tables, columns, and relationships you need
- Review the SQL: Claude shows you the migration before running it
- Apply the migration:
apply_migrationruns it and records it - Add RLS policies: describe who can read and write each table
- Verify the types:
generate_typescript_typessyncs your TypeScript - Wire auth: tell Claude to add route protection for any page that needs it
- 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.
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。