Cloud-native data warehouse AnalyticDB for PostgreSQL integrates with the open-source Supabase authentication system. This enables enterprises to configure SAML 2.0 single sign-on (SSO), making it ideal for building private Coding Agent platforms. This document explains how to configure SAML SSO for a Supabase project to unify identity authentication for enterprise users.
Prerequisites
You have created a Supabase project. The project must be created after the following dates:
China region: April 1, 2026.
Singapore: April 1, 2025.
If your SSO identity provider (IdP) and Supabase are not in the same VPC, you must enable public access for Supabase.
Procedure
Step 1: Obtain Supabase project information
After creating a Supabase project, you can obtain the following information.
Item | Description | Example |
Supabase URL | The access URL of your Supabase project. |
|
Anon Key (public key) | A public key that can be safely used in your frontend code. |
|
Service Role Key (secret key) | A secret key for backend use only. It is used for administrative operations. Never expose this key in your frontend code. |
|
Verify project status
curl 'https://<your-supabase-url>/auth/v1/health' \
-H 'apikey: <your-anon-key>'Example response
{"version":"vunspecified","name":"GoTrue","description":"GoTrue is a user registration and authentication API"}Step 2: Obtain SP metadata
The metadata for the SAML service provider (SP) is publicly accessible at the following URL:
https://<your-supabase-url>/sso/saml/metadataYou will need the following two URLs to configure your IdP:
URL type | Value |
ACS URL |
|
Entity ID |
|
Step 3: Configure redirect URL
Log in to the Supabase Dashboard. In the sidebar of the Dashboard, click Authentication > URL Configuration to navigate to the Site URL configuration page.
In the Site URL section, enter your frontend domain URL, and then click Save changes.
Step 4: Register identity provider (IdP)
Register your IdP with Supabase using the Management API.
Method 1: Register via metadata URL
If your IdP provides a public metadata URL (as most do), run the following command:
curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
-H 'apikey: <your-service-role-key>' \
-H 'Authorization: Bearer <your-service-role-key>' \
-H 'Content-Type: application/json' \
-d '{
"type": "saml",
"metadata_url": "https://<your-idp>/samlp/metadata/<client-id>",
"domains": ["yourdomain.com"]
}'Method 2: Register via metadata XML
If the metadata URL is not publicly accessible, you can provide the XML content directly:
# 1. Obtain the metadata XML from an environment that can access the IdP.
curl 'https://<your-idp>/samlp/metadata/<client-id>' > idp-metadata.xml
# 2. Register with Supabase.
curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
-H 'apikey: <your-service-role-key>' \
-H 'Authorization: Bearer <your-service-role-key>' \
-H 'Content-Type: application/json' \
-d '{
"type": "saml",
"metadata_xml": "<EntityDescriptor ...>...</EntityDescriptor>",
"domains": ["yourdomain.com"]
}'Example: Auth0 registration
The Auth0 metadata URL has the following format:
https://<tenant>.auth0.com/samlp/metadata/<client-id>The complete registration command is as follows:
curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
-H 'apikey: <your-service-role-key>' \
-H 'Authorization: Bearer <your-service-role-key>' \
-H 'Content-Type: application/json' \
-d '{
"type": "saml",
"metadata_url": "https://dev-xxx.auth0.com/samlp/metadata/YOUR_CLIENT_ID",
"domains": ["example.com"]
}'Save returned provider ID
Save the id field from the response. This ID is required when initiating a sign-in request from the frontend.
The following is an example response for a successful registration:
{
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"type": "saml",
"saml": {
"entity_id": "urn:dev-xxx.auth0.com"
},
"domains": [{"domain": "example.com"}]
}Step 5: Configure SP information in IdP
After registration, configure the Supabase SP information in your IdP.
Required configuration items
Parameter | Value |
ACS URL (callback URL) |
|
Entity ID (SP Entity ID) |
|
NameID Format |
|
Frontend callback URL |
|
Example: Configuration steps for Auth0
Log in to the Auth0 Dashboard.
Go to Applications and click the target application.
Click the Addons tab and enable SAML2 Web App.
In the dialog box that appears, configure the following settings:
Application Callback URL
Settings
Click Enable, and then click Save.
On the application's main page, add your frontend URL to Allowed Callback URLs.
Step 6: Frontend integration
Install the dependency.
npm install @supabase/supabase-jsInitialize the client. SAML SSO must use the
implicitflow.import { createClient } from '@supabase/supabase-js' const supabase = createClient( 'https://<your-supabase-url>', '<your-anon-key>', { auth: { flowType: 'implicit', detectSessionInUrl: true, persistSession: true, autoRefreshToken: true, } } )Initiate SSO sign-in.
Initiate sign-in using the provider ID:
const { data, error } = await supabase.auth.signInWithSSO({ providerId: '<your-provider-id>', options: { redirectTo: window.location.origin } }) if (error) { console.error('Sign-in failed:', error.message) return } if (data?.url) { window.location.href = data.url }Alternatively, automatically match the provider using a domain:
const { data, error } = await supabase.auth.signInWithSSO({ domain: 'yourdomain.com' })
Handle the sign-in callback.
After the IdP authenticates the user, it redirects back to the frontend. The URL has the following format:
https://<your-app-domain>/#access_token=<jwt>&refresh_token=<token>&expires_in=3600&token_type=bearerBecause the supabase-js initialization is asynchronous, we recommend that you manually parse the URL hash:
function decodeJWT(token) { try { return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))) } catch { return null } } async function init() { const hash = window.location.hash.substring(1) if (hash.includes('access_token')) { const params = new URLSearchParams(hash) const accessToken = params.get('access_token') const refreshToken = params.get('refresh_token') const expiresAt = params.get('expires_at') window.history.replaceState({}, '', window.location.pathname) const payload = decodeJWT(accessToken) if (payload) { const session = { access_token: accessToken, refresh_token: refreshToken || '', expires_at: expiresAt ? parseInt(expiresAt) : payload.exp, token_type: 'bearer', user: { id: payload.sub, email: payload.email, role: payload.role, app_metadata: payload.app_metadata || {}, user_metadata: payload.user_metadata || {}, } } onLoginSuccess(session) return } } const { data: { session } } = await supabase.auth.getSession() if (session) { onLoginSuccess(session) } else { showLoginPage() } } document.addEventListener('DOMContentLoaded', init)View the JWT payload structure.
After a successful sign-in, the decoded JWT contains the following key fields:
{ "sub": "8c308927-4ba1-4507-b6f8-f751b791****", "email": "user@example.com", "role": "authenticated", "aal": "aal1", "amr": [{ "method": "sso/saml", "timestamp": 1774928127, "provider": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }], "app_metadata": { "provider": "sso:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }, "session_id": "aeaf9cdb-dbcd-46ab-95a5-ff956e69****" }You can confirm that the user signed in through SSO by checking if
amr[0].method === 'sso/saml'.
Complete example project
Here is a minimal, runnable example. For an online demo, see sso-demo.
main.js
import { createClient } from '@supabase/supabase-js' const SUPABASE_URL = 'https://<your-supabase-url>' const SUPABASE_ANON_KEY = '<your-anon-key>' const AUTH0_PROVIDER_ID = '<auth0-provider-id>' const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { auth: { flowType: 'implicit', detectSessionInUrl: true } }) function decodeJWT(token) { try { return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))) } catch { return null } } window.signInWithAuth0 = async () => { const { data, error } = await supabase.auth.signInWithSSO({ providerId: AUTH0_PROVIDER_ID, options: { redirectTo: window.location.origin } }) if (error) return alert(error.message) if (data?.url) window.location.href = data.url } window.signOut = async () => { await supabase.auth.signOut() window.location.reload() } async function init() { const hash = window.location.hash.substring(1) if (hash.includes('access_token')) { const p = new URLSearchParams(hash) const token = p.get('access_token') const payload = decodeJWT(token) window.history.replaceState({}, '', window.location.pathname) if (payload) { showUser({ token, payload, refresh_token: p.get('refresh_token') }) return } } const { data: { session } } = await supabase.auth.getSession() if (session) { showUser({ token: session.access_token, payload: decodeJWT(session.access_token) }) } } function showUser({ token, payload }) { document.getElementById('login').style.display = 'none' document.getElementById('user').innerHTML = ` <p><strong>Email: </strong>${payload.email}</p> <p><strong>User ID: </strong>${payload.sub}</p> <p><strong>Authentication Method: </strong>${payload.amr?.[0]?.method}</p> <button onclick="signOut()">Sign out</button> ` } document.addEventListener('DOMContentLoaded', init)index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SSO Login</title> </head> <body> <div id="login"> <button onclick="signInWithAuth0()">Sign in with Auth0</button> </div> <div id="user"></div> <script type="module" src="/main.js"></script> </body> </html>
The code above implements an Auth0 SSO sign-in flow.
You can view SSO user records in the auth.users table of your Supabase project. The is_sso_user field for these users is set to TRUE.
Manage providers
You can use the following API operations to add, delete, or manage SSO providers. All of these operations require the service role key.
List providers
curl 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
-H 'apikey: <your-service-role-key>' \
-H 'Authorization: Bearer <your-service-role-key>'You can also view the auth.sso_domains and auth.sso_providers tables in the Supabase Dashboard to confirm the registered SSO providers.
Get provider
curl 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
-H 'apikey: <your-service-role-key>' \
-H 'Authorization: Bearer <your-service-role-key>'Update provider
curl -X PUT 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
-H 'apikey: <your-service-role-key>' \
-H 'Authorization: Bearer <your-service-role-key>' \
-H 'Content-Type: application/json' \
-d '{"metadata_url": "https://<your-idp>/samlp/metadata/<client-id>"}'Delete provider
curl -X DELETE 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
-H 'apikey: <your-service-role-key>' \
-H 'Authorization: Bearer <your-service-role-key>'FAQ
Page does not redirect after sign-in
Confirm that flowType: 'implicit' is set in the createClient configuration. The default Proof Key for Code Exchange (PKCE) flow may cause issues in SAML callback scenarios.
Sign-in page remains after callback
The SDK initializes asynchronously, so you must manually parse the access_token from the URL hash when the DOMContentLoaded event occurs. Do not rely solely on supabase.auth.getSession() to handle the callback.
Sign-in failed: No such SSO provider
Check whether the
providerIdin your frontend code matches the ID that was returned during registration.Perform a hard refresh of your browser (Cmd+Shift+R or Ctrl+Shift+R) to clear the cache.
Sign-in failed: Failed to fetch
Check your network connection to the Supabase project.
Run a test command in the browser console to verify connectivity.
Incorrect redirect after sign-in
Explicitly specify the frontend URL in the options.redirectTo parameter of the signInWithSSO() function. This parameter overrides the instance's default Site URL.
SAML RelayState has expired
The SSO sign-in URL is time-sensitive and typically expires in a few minutes. Call signInWithSSO() for each new sign-in attempt. Do not reuse an old sign-in URL.
Auth0 error: invalid_request
Check that Allowed Callback URLs in your Auth0 settings includes your frontend domain, and that the Application Callback URL is set to the Supabase ACS URL.