Supabase as an OAuth Provider: Self-Hosted Setup Guide

Turn your self-hosted Supabase into an OAuth 2.1 and OIDC identity provider. GoTrue config, client registration, consent screens, and MCP auth explained.

Cover Image for Supabase as an OAuth Provider: Self-Hosted Setup Guide

Every guide about OAuth and Supabase — including our own — covers the same direction: your app consumes identity from Google, GitHub, or Discord. But since late 2025, Supabase Auth can flip that relationship. The OAuth 2.1 server feature turns your project into a full identity provider, so third-party apps, partner integrations, and AI agents can offer "Sign in with your app" — backed by the user table you already have.

On Supabase Cloud, this is a dashboard toggle. On self-hosted, there's no toggle — it's a handful of undocumented GoTrue environment variables, a consent screen you build yourself, and a couple of gateway routing gotchas. This guide covers the whole path: enabling the server, registering OAuth clients via the Admin API, building the consent flow, and the trade-offs you should know before shipping it.

Why This Feature Matters in 2026

Three things are driving interest in Supabase-as-identity-provider:

  1. MCP servers and AI agents. The Model Context Protocol spec settled on OAuth 2.1 for authorization. If you're building an MCP server on top of your product, agents like Claude need to authenticate as one of your users — and an OAuth server that speaks dynamic client registration is exactly what the spec expects. (This is the flip side of using MCP as a coding tool against your instance.)
  2. Partner integrations. Instead of minting long-lived API keys for every partner, you issue scoped, refreshable OAuth tokens. Row Level Security applies to those tokens automatically, because they're ordinary Supabase JWTs with a client_id claim added.
  3. OIDC for enterprise customers. Larger customers increasingly ask "can we get OIDC from you?" — and now the answer for a self-hosted deployment can be yes, without standing up Keycloak next to your stack.

The feature is in public beta (announced November 2025), free on all plans, and — critically for us — it ships in the open-source supabase/auth (GoTrue) image, so self-hosters get it too.

Prerequisites

Before enabling anything, check three things:

  • A recent auth image. You need a supabase/auth version from late 2025 or newer. If you pinned images during the Kong-to-Envoy migration and haven't bumped since, update auth first.
  • Asymmetric JWT signing. OAuth clients validate your tokens against your JWKS endpoint. That only works with RS256/ES256 keys. If you're still on the default shared-secret HS256 setup, ID token generation for the openid scope will fail outright — this is a hard requirement, not a recommendation.
  • A stable public issuer URL. Discovery documents embed your API URL. If you're still serving Supabase off a raw IP or a changing tunnel hostname, fix that first with a custom domain — token validation breaks every time the issuer changes.

Enabling the OAuth Server in Docker Compose

The official docs only show the config.toml syntax for the CLI's local dev stack:

[auth.oauth_server]
enabled = true
authorization_url_path = "/oauth/consent"
allow_dynamic_registration = false

For a production Docker Compose deployment, those settings map to GoTrue environment variables (confirmed against the supabase/auth configuration source):

auth:
  environment:
    GOTRUE_OAUTH_SERVER_ENABLED: "true"
    GOTRUE_OAUTH_SERVER_AUTHORIZATION_PATH: "/oauth/consent"
    GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION: "false"
    # optional — defaults shown
    GOTRUE_OAUTH_SERVER_AUTHORIZATION_TTL: "10m"
    GOTRUE_OAUTH_SERVER_DEFAULT_SCOPE: "email"

AUTHORIZATION_PATH is joined with your Site URL to produce the consent screen address — so https://app.example.com + /oauth/consent means your frontend must serve a page at https://app.example.com/oauth/consent. More on building that below.

Leave ALLOW_DYNAMIC_REGISTRATION off unless you're specifically serving MCP clients. Dynamic registration lets anyone who can reach your auth endpoint register an OAuth client without authentication. That's what the MCP spec requires, but it also means your instance will accept client registrations from the open internet — if you enable it, make sure rate limiting at your gateway is in place.

After a docker compose up -d auth, verify the discovery endpoints respond:

curl https://api.example.com/.well-known/oauth-authorization-server/auth/v1
curl https://api.example.com/auth/v1/.well-known/openid-configuration

Gateway gotcha: the OAuth authorization-server discovery document lives at the root path (/.well-known/oauth-authorization-server/auth/v1), not under /auth/v1/ like everything else. Default Kong/Envoy configs route /auth/v1/* to GoTrue but may not route root-level /.well-known/* paths. If that first curl returns a gateway 404, add a route for /.well-known/oauth-authorization-server pointing at the auth service. Standards-compliant clients (including MCP clients) resolve this path automatically and will fail opaquely without it.

Registering OAuth Clients

Here's the first honest trade-off: on self-hosted, don't count on the Studio dashboard exposing the Authentication → OAuth Apps UI that Cloud has. Self-hosted Studio historically lags Cloud's auth UI (the same reason provider config is env-var-driven). Register clients through the Admin API instead — it works identically everywhere:

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(SUPABASE_URL, SERVICE_ROLE_KEY);

const { data, error } = await supabase.auth.admin.oauth.createClient({
  name: 'Partner Dashboard',
  redirect_uris: ['https://partner.example.com/auth/callback'],
  client_type: 'confidential',
  token_endpoint_auth_method: 'client_secret_basic',
});
// data contains client_id and (for confidential clients) client_secret

Two rules that trip people up:

  • Redirect URIs are exact-match only. No wildcards, no path prefixes — unlike Supabase's general redirect URL allow-list. https://partner.example.com/auth/callback will not match https://partner.example.com/auth/callback/.
  • Client types matter. Use confidential for server-side apps that can keep a secret; use public (with token_endpoint_auth_method: 'none') for SPAs, native apps, and MCP clients. All flows use authorization code + PKCE — implicit flow doesn't exist in OAuth 2.1.

You can audit what's registered at any time with supabase.auth.admin.oauth.listClients() — worth wiring into your monitoring if dynamic registration is on.

GoTrue handles the protocol; you own the UI. When a client starts an authorization flow, the user lands on your AUTHORIZATION_PATH page with an authorization_id query parameter. Your page does three things:

// 1. Fetch what's being requested
const { data } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId);
// data includes the client name and a space-separated `scope` string

// 2a. User clicks "Allow"
const { data: approval } = await supabase.auth.oauth.approveAuthorization(authorizationId);
// redirect the browser to approval.redirect_to

// 2b. User clicks "Deny"
await supabase.auth.oauth.denyAuthorization(authorizationId);

The user must already be signed in to your app for this to work — if they aren't, send them through your normal login first and bounce them back to the consent URL. Authorizations expire after AUTHORIZATION_TTL (10 minutes by default), so don't park users on this page.

The issued access tokens are standard Supabase JWTs carrying user_id, role, and client_id claims. Your existing RLS policies apply with zero changes — a partner app querying PostgREST with an OAuth token sees exactly what that user can see. If you want per-client behavior (say, a reduced audience for agent traffic), a custom access token hook receives the client_id and can shape claims accordingly.

Honest Limitations

Before you build a business feature on this, know what you're signing up for:

  • It's beta. Endpoint shapes and SDK method names may shift before GA. Pin your auth image version and read release notes before bumping — the June 2026 breaking changes showed how quickly auth paths can move.
  • Scopes are coarse. Granular, resource-level scopes are on Supabase's roadmap but not shipped. Today you're mostly gating on "is this user authenticated via this client" plus RLS — fine for MCP and first-party partners, thin for a public API platform.
  • You're the IdP now. Uptime expectations change when other companies' login buttons depend on your instance. Treat the auth service as tier-one: monitor it, back it up, and test restores — client registrations and refresh tokens live in your auth schema, so your backup strategy now protects other people's integrations too.
  • Branding still applies. The consent screen is yours, but the URLs users see are your API domain's. If you haven't already, sort out clean OAuth branding so the flow doesn't look like a phishing page.

Where Supascale Fits

The fiddly part of this feature on self-hosted infrastructure isn't the protocol — it's the plumbing around it: a stable HTTPS issuer URL, correct gateway routes, safe auth-image upgrades, and backups that actually capture the auth schema. That's the layer Supascale manages. Custom domains with automatic SSL give you a permanent issuer URL, service-level configuration keeps GoTrue env vars versioned instead of scattered across a .env file, and scheduled S3 backups with one-click restore cover the database your OAuth clients now live in. A one-time license covers unlimited projects, so a staging instance for testing beta auth features costs nothing extra.

Conclusion

The OAuth 2.1 server quietly changes what a self-hosted Supabase deployment can be: not just a backend, but an identity provider for an ecosystem — partner apps, enterprise OIDC, and the fast-growing population of MCP-speaking AI agents. Enabling it takes five environment variables and a consent page; running it well takes a stable domain, a routed /.well-known path, asymmetric JWTs, and real backup discipline. Start with dynamic registration off, register one confidential client, and expand once the flow is boring. Boring is exactly what you want from an identity provider.

Further Reading