Auth & Identity8 min read

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.

Published April 29, 2026
01Overview

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.


02The case for integration

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.


03Architecture

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.


04Use cases

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.


05Implementation

Step-by-step integration

  1. 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. 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. 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. 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. 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. 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.


06Code

Code example

typescriptapp/api/sanity-to-supabase-auth/route.ts
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 });
}

07Why Sanity

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 →

08Comparison

CMS approaches to Supabase Auth

CapabilityTraditional CMSSanity
Auth metadata sourceRole, plan, tenant, and gated section documents live as typed JSON in the Content Lake and can sync to Supabase Auth on publish.
Publish-triggered updatesGROQ-powered webhooks and Functions can trigger only for access-related changes, then update Supabase Auth without running separate infrastructure.
Field-level sync controlGROQ can project a compact payload with joined references, such as role, plan, tenant, locale, and gated section slugs.
Multi-tenant access rulesSanity Studio can give editors specific tenant, organization, and plan interfaces while Functions sync the resulting rules to Supabase Auth.
Frontend gatingSupabase Auth handles sessions and JWTs, while Sanity supplies the structured content and publish events that keep claims and gated experiences aligned.
AI agent contextAgent Context can give production AI agents scoped, read-only access to structured content while Supabase Auth continues to handle user identity.

09Next steps

Keep building

Explore related integrations to complete your content stack.

Ready to try Sanity?

See how Sanity's Content Operating System powers integrations with Supabase Auth and 200+ other tools.