How to Integrate Supabase Auth with Your Headless CMS
Connect Supabase Auth to your headless CMS so roles, plans, invites, and gated content rules update the moment content teams publish changes.
What is Supabase Auth?
Supabase Auth is the authentication service built into Supabase, the open-source backend platform based on Postgres. It handles email and password login, magic links, OAuth providers, phone auth, JWT sessions, user metadata, and admin user operations through the Supabase SDK and Auth API. Teams often use it when they want authentication that works closely with Postgres row-level security, web apps, mobile apps, and server-side API routes.
Why integrate Supabase Auth with a headless CMS?
Auth rules rarely live in one place for long. A marketing site might have public articles, member-only tutorials, partner-only downloads, and region-specific onboarding pages. If those access rules live in code while the content lives somewhere else, every pricing change or partner launch becomes a developer ticket.
Architecture overview
A typical integration starts with an access-related document in Sanity Studio, for example an accessProfile, organization, plan, or gatedContentRule. When that document is published, a Sanity webhook fires with the document ID, or a Function runs directly from the content mutation. The handler uses @sanity/client and GROQ to fetch exactly the fields Supabase Auth needs, such as email, supabaseUserId, role, plan, tenantId, and referenced gated section slugs. Then it calls the Supabase Admin API with a service role key, usually supabase.auth.admin.updateUserById() to update app_metadata or supabase.auth.admin.inviteUserByEmail() to create an invited user. Supabase Auth issues JWT sessions to the end user, and your frontend or middleware reads those claims to decide which Sanity-powered pages, tools, or API routes the user can access. Keep the full content in the Content Lake. Supabase Auth should hold identity and access metadata, not article bodies or large content payloads.
Common use cases
Role-based content access
Publish role and plan rules in Sanity, then sync them to Supabase Auth app_metadata for checks in middleware, server components, or API routes.
Invite-only member portals
Create a member profile in Sanity, publish it, and call Supabase Auth's inviteUserByEmail() to send the signup flow automatically.
Multi-tenant onboarding
Map Sanity organization documents to Supabase Auth user metadata so each customer sees the right onboarding copy, plan limits, and workspace links.
Localized account experiences
Use Sanity to define locale, region, and audience rules, then sync those values to Supabase Auth so signed-in users land in the right experience.
Step-by-step integration
- 1
Set up Supabase Auth
Create a Supabase project, enable the providers you need under Authentication, such as email, magic links, Google, GitHub, or SSO, and copy your project URL, anon key, and service_role key. Install the SDK with npm install @supabase/supabase-js. Keep the service_role key server-side only.
- 2
Model access content in Sanity Studio
Create a schema for accessProfile, plan, organization, or gatedContentRule. Include fields like email, supabaseUserId, role, plan, tenantId, locale, and references to gated sections. Add an authSyncStatus field if you want editors to see whether the last sync worked.
- 3
Write the GROQ projection
Use GROQ to fetch only the fields needed for Supabase Auth. For example, pull the user's role, plan, tenant ID, and gated section slugs through references instead of sending the whole document.
- 4
Create the sync mechanism
Use a Sanity webhook filtered to published access documents, or run a Function when those documents change. The handler should receive the Sanity document ID, fetch the latest published version, and call the Supabase Admin API.
- 5
Call Supabase Auth from the server
Use supabase.auth.admin.updateUserById() for existing users, inviteUserByEmail() for new invited users, and app_metadata for server-trusted authorization values. Avoid storing sensitive access rules in user_metadata because users can update parts of their own profile.
- 6
Test the signed-in experience
Sign in with a test user, publish a role or plan change in Sanity, confirm the webhook runs, and refresh the Supabase session so the JWT includes the latest metadata. Then test your frontend route guards, API checks, and gated Sanity queries.
Code example
import { createClient as createSanityClient } from '@sanity/client';
import { createClient as createSupabaseClient } from '@supabase/supabase-js';
import type { NextRequest } from 'next/server';
const sanity = createSanityClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_READ_TOKEN!,
useCdn: false,
});
const supabase = createSupabaseClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { persistSession: false } }
);
export async function POST(req: NextRequest) {
const { _id } = await req.json();
const profile = await sanity.fetch(
`*[_id == $id][0]{
email,
supabaseUserId,
role,
plan,
tenantId,
"sections": gatedSections[]->slug.current
}`,
{ id: _id.replace('drafts.', '') }
);
if (!profile?.email) {
return Response.json({ skipped: true }, { status: 404 });
}
const app_metadata = {
role: profile.role,
plan: profile.plan,
tenant_id: profile.tenantId,
sections: profile.sections ?? [],
};
if (profile.supabaseUserId) {
const { error } = await supabase.auth.admin.updateUserById(
profile.supabaseUserId,
{ app_metadata }
);
if (error) return Response.json({ error: error.message }, { status: 500 });
} else {
const invited = await supabase.auth.admin.inviteUserByEmail(profile.email, {
data: { plan: profile.plan },
});
if (invited.error) {
return Response.json({ error: invited.error.message }, { status: 500 });
}
await supabase.auth.admin.updateUserById(invited.data.user.id, { app_metadata });
}
return Response.json({ ok: true });
}How Sanity + Supabase Auth works
Build your Supabase Auth integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect Supabase Auth with the experiences your signed-in users see.
Start building free →CMS approaches to Supabase Auth
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Auth metadata source | Role, plan, tenant, and gated section documents live as typed JSON in the Content Lake and can sync to Supabase Auth on publish. | |
| Publish-triggered updates | GROQ-powered webhooks and Functions can trigger only for access-related changes, then update Supabase Auth without running separate infrastructure. | |
| Field-level sync control | GROQ can project a compact payload with joined references, such as role, plan, tenant, locale, and gated section slugs. | |
| Multi-tenant access rules | Sanity Studio can give editors specific tenant, organization, and plan interfaces while Functions sync the resulting rules to Supabase Auth. | |
| Frontend gating | Supabase Auth handles sessions and JWTs, while Sanity supplies the structured content and publish events that keep claims and gated experiences aligned. | |
| AI agent context | Agent Context can give production AI agents scoped, read-only access to structured content while Supabase Auth continues to handle user identity. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Auth0
Connect structured roles, organizations, and gated content rules with Auth0 tenants, apps, and enterprise login flows.
Sanity + Clerk
Use Sanity to define signed-in experiences while Clerk handles user profiles, organizations, sessions, and frontend auth components.
Sanity + Firebase Auth
Sync audience and access metadata from Sanity to Firebase Auth for web and mobile apps using Firebase sessions.