Claude Code + Supabase Integration Guide
To use Supabase with Claude Code, paste your schema or Supabase dashboard output directly into the chat and ask Claude to generate typed clients, Row Level Security policies, Auth flows, or Edge Functions against it. Claude understands Supabase's conventions — createClient, supabase.auth, supabase.from(), .subscribe(), Storage buckets, and supabase gen types typescript — so you can go from a blank project to a fully secured, realtime Next.js app in a single session without leaving your editor.
Project Setup
Start every Supabase + Next.js project the same way so Claude Code has full context.
Install dependencies:
npm install @supabase/supabase-js @supabase/ssr
Environment variables (.env.local):
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # server-side only
Prompt to bootstrap clients with Claude Code:
Create two Supabase client files for Next.js 15 App Router:
1. lib/supabase/client.ts — browser client using createBrowserClient
2. lib/supabase/server.ts — server component client using createServerClient with cookies()
Use @supabase/ssr. Include full TypeScript types from ./database.types.ts
Browser client (lib/supabase/client.ts):
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "./database.types";
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
Server client (lib/supabase/server.ts):
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "./database.types";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
},
},
}
);
}
For database migration workflows that complement this setup, see Claude Code database migrations.
Postgres Schema with Row Level Security
Define your schema in .sql files and paste them into Claude Code to generate matching RLS policies, TypeScript types, and query helpers.
Example schema (tables for a SaaS app):
-- Enable RLS on all tables
create table public.organizations (
id uuid primary key default gen_random_uuid(),
name text not null,
created_at timestamptz default now()
);
create table public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
org_id uuid references public.organizations(id),
role text not null check (role in ('owner', 'admin', 'member')),
full_name text,
avatar_url text,
updated_at timestamptz default now()
);
create table public.documents (
id uuid primary key default gen_random_uuid(),
org_id uuid not null references public.organizations(id),
owner_id uuid not null references public.profiles(id),
title text not null,
content text,
is_public boolean default false,
created_at timestamptz default now()
);
alter table public.organizations enable row level security;
alter table public.profiles enable row level security;
alter table public.documents enable row level security;
Complete RLS Policy Example
The following policies for documents demonstrate the four standard RLS operations — SELECT, INSERT, UPDATE, DELETE — with role-aware logic.
-- SELECT: members see their org's documents; public docs visible to all
create policy "documents_select"
on public.documents for select
using (
is_public = true
or org_id in (
select org_id from public.profiles
where id = auth.uid()
)
);
-- INSERT: only org members can create documents in their org
create policy "documents_insert"
on public.documents for insert
with check (
org_id in (
select org_id from public.profiles
where id = auth.uid()
)
and owner_id = auth.uid()
);
-- UPDATE: document owner OR org admin can edit
create policy "documents_update"
on public.documents for update
using (
owner_id = auth.uid()
or exists (
select 1 from public.profiles
where id = auth.uid()
and org_id = documents.org_id
and role in ('owner', 'admin')
)
);
-- DELETE: only document owner or org owner can delete
create policy "documents_delete"
on public.documents for delete
using (
owner_id = auth.uid()
or exists (
select 1 from public.profiles
where id = auth.uid()
and org_id = documents.org_id
and role = 'owner'
)
);
How these policies work: Every query runs as auth.uid() — the JWT user ID. The SELECT policy uses an in subquery against profiles so members only see their organization's data, while is_public allows unauthenticated reads for public documents. The UPDATE policy uses exists to check role without a join, keeping the plan efficient. Admins can edit any document; owners can delete but members cannot.
Prompt to generate RLS policies with Claude Code:
Given this schema [paste SQL], generate RLS policies for every table.
Rules:
- Users can only read rows that belong to their org_id (from profiles)
- Only 'owner' and 'admin' roles can delete
- Service role bypasses all policies (do not add service role checks)
Output as a single .sql migration file.
For security scanning of generated policies, see Claude Code security scanning.
Supabase Auth: Email and OAuth
Prompt for full auth setup:
Set up Supabase Auth for Next.js App Router with:
- Email/password sign-up with email confirmation
- Google OAuth
- Middleware that protects /dashboard/** routes
- A server action for sign-out
Use @supabase/ssr. Redirect to /auth/callback after OAuth.
Auth callback route (app/auth/callback/route.ts):
import { createClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
const next = searchParams.get("next") ?? "/dashboard";
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
return NextResponse.redirect(`${origin}/auth/error`);
}
Middleware (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 }) =>
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("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return supabaseResponse;
}
export const config = {
matcher: ["/dashboard/:path*"],
};
Realtime Subscriptions
Claude Code can generate typed realtime listeners from your schema.
Prompt:
Using the documents table schema above, generate a React hook
useDocumentChanges(orgId: string) that subscribes to INSERT, UPDATE,
and DELETE events on documents where org_id = orgId.
Update local state optimistically. Clean up on unmount.
Generated hook (hooks/use-document-changes.ts):
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import type { Database } from "@/lib/supabase/database.types";
type Document = Database["public"]["Tables"]["documents"]["Row"];
export function useDocumentChanges(orgId: string) {
const [documents, setDocuments] = useState<Document[]>([]);
const supabase = createClient();
useEffect(() => {
// Initial fetch
supabase
.from("documents")
.select("*")
.eq("org_id", orgId)
.then(({ data }) => {
if (data) setDocuments(data);
});
// Realtime subscription
const channel = supabase
.channel(`documents:${orgId}`)
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "documents",
filter: `org_id=eq.${orgId}`,
},
(payload) => {
if (payload.eventType === "INSERT") {
setDocuments((prev) => [...prev, payload.new as Document]);
} else if (payload.eventType === "UPDATE") {
setDocuments((prev) =>
prev.map((d) =>
d.id === payload.new.id ? (payload.new as Document) : d
)
);
} else if (payload.eventType === "DELETE") {
setDocuments((prev) =>
prev.filter((d) => d.id !== payload.old.id)
);
}
}
)
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [orgId]);
return documents;
}
Storage for File Uploads
Prompt to generate a typed upload helper:
Create a TypeScript function uploadDocumentAttachment(file: File, docId: string)
that uploads to the 'attachments' Supabase Storage bucket under path
{orgId}/{docId}/{filename}, returns the public URL, and handles errors.
Add an RLS policy so only the document owner can upload to their path.
Storage RLS policy:
-- Allow authenticated users to upload to their own paths
create policy "attachments_upload"
on storage.objects for insert
with check (
bucket_id = 'attachments'
and auth.uid()::text = (storage.foldername(name))[2]
);
-- Allow org members to read attachments
create policy "attachments_read"
on storage.objects for select
using (
bucket_id = 'attachments'
and auth.uid() in (
select id from public.profiles
where org_id::text = (storage.foldername(name))[1]
)
);
Edge Functions
Prompt to scaffold an Edge Function:
Create a Supabase Edge Function at supabase/functions/process-document/index.ts
that:
1. Verifies the JWT from the Authorization header
2. Reads a document by ID from the request body
3. Calls an external API to process it
4. Updates the document status in Postgres
5. Returns the result as JSON
Include CORS headers for claudeguide.io.
Deploy and invoke:
supabase functions deploy process-document
supabase functions invoke process-document --body '{"docId":"abc-123"}'
Type Generation with supabase gen types
After every schema change, regenerate types so Claude Code works with fresh definitions.
supabase gen types typescript --project-id your-project-id \
--schema public > lib/supabase/database.types.ts
Automate in package.json:
{
"scripts": {
"types": "supabase gen types typescript --project-id $SUPABASE_PROJECT_ID --schema public > lib/supabase/database.types.ts"
}
}
Prompt after running type generation:
I just regenerated database.types.ts. Review all files in lib/ and app/
that import from database.types.ts and update any TypeScript errors
caused by schema changes. Show me a diff for each file.
Claude reads the updated types file and propagates changes through your codebase, fixing type mismatches before they hit the compiler. For the complete Claude Code workflow reference, see Claude Code complete guide.
Gumroad CTA
Spending time hunting for the right Supabase + Claude prompt for each situation?
Claude Code Power Prompts 300 includes 30+ Supabase-specific prompts — covering RLS policy generation, Auth setup, realtime hooks, Storage policies, and Edge Function scaffolding. Every prompt is production-tested and ready to paste.
→ Get Claude Code Power Prompts 300 — $29
300 prompts. Instant download. 30-day money-back guarantee.
Frequently Asked Questions
How do I use Supabase with Claude Code for a Next.js project?
Install @supabase/supabase-js and @supabase/ssr, add your NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY to .env.local, then paste your schema into Claude Code and ask it to generate the browser client, server client, middleware, and Auth callback route. Claude generates all four files in one prompt and keeps TypeScript types consistent across them.
Can Claude Code generate Row Level Security policies automatically?
Yes. Paste your table definitions and describe your access rules in plain English — for example, "users can only read rows belonging to their organization" — and Claude generates the corresponding CREATE POLICY SQL. For multi-role apps, describe each role explicitly: "admins can update any row, members can only update rows they own."
How do I keep TypeScript types in sync after schema changes?
Run supabase gen types typescript after every migration and commit the updated database.types.ts. Then prompt Claude: "I regenerated database.types.ts — fix all TypeScript errors in the codebase caused by these schema changes." Claude diffs the old and new types and patches every affected file.
Does RLS protect my data even if someone bypasses the UI?
Yes, RLS enforces policies at the database level regardless of how the query arrives. Direct supabase-js calls, REST calls, and GraphQL queries all go through RLS. The only bypass is the service_role key — never expose it client-side, and never use it in browser code. Always use the anon key in your browser client.
How do I test RLS policies locally?
Use the Supabase local development stack (supabase start) and run queries as specific users with set local role authenticated; set local "request.jwt.claims" = '{"sub":"<user-id>"}'. Prompt Claude: "Write pgTAP tests for these RLS policies that simulate three different user roles and verify access is granted or denied correctly."
Take It Further
Want 300 ready-to-use prompts for every Claude Code workflow — including 30+ Supabase prompts for schema design, RLS, Auth, and Edge Functions?
Claude Code Power Prompts 300 has you covered. Every prompt is organized by use case and tested in real projects.
→ Get Claude Code Power Prompts 300 — $29
Instant download. 30-day money-back guarantee.