Configure Custom OAuth/OIDC Providers
Set up any OAuth 2.0 or OIDC-compatible identity provider for self-hosted Supabase.
This guide explains how to add a custom OAuth 2.0 or OpenID Connect (OIDC) identity provider to a self-hosted Supabase instance and how to manage it through the Auth admin API. To learn how custom providers work and which advanced options they support, see Custom OAuth/OIDC Providers.
There are two provider types:
- OAuth 2.0: for generic OAuth 2.0 providers where you supply the authorization, token, and userinfo endpoints manually.
- OIDC: for providers that support OpenID Connect discovery. You supply only the issuer URL and endpoints are resolved automatically.
Provider identifiers#
Every custom provider identifier must start with the custom: prefix. Identifiers are 2-50 characters, lowercase alphanumeric with hyphens and colons allowed. Examples:
custom:my-providercustom:github-enterprise
Before you begin#
You need:
- A working self-hosted Supabase instance on release
0.8.1or later, which ships Supabase Authv2.196.0. See Self-Hosting with Docker and Update Your Self-Hosted Deployment. - Your project's secret key,
SUPABASE_SECRET_KEY, from your.envfile. API_EXTERNAL_URLset to the publicly reachable URL of your Auth service, ending in/auth/v1, for examplehttps://<your-domain>/auth/v1. The Auth service derives the OAuth callback URL for custom providers from this value.
Most OAuth providers reject http:// callback URLs other than localhost, so your instance needs HTTPS. See Configure Reverse Proxy and HTTPS for setup instructions.
When registering your application with an external identity provider, add the following URL as the redirect URI in the provider's settings:
https://<your-domain>/auth/v1/callbackOptional Auth configuration#
Custom providers are enabled by default in the Auth service, so no changes to docker-compose.yml are required. The following environment variables adjust the defaults:
| Variable | Description |
|---|---|
GOTRUE_CUSTOM_OAUTH_ENABLED | Set to "false" to disable custom OAuth/OIDC providers. Defaults to true. |
GOTRUE_CUSTOM_OAUTH_MAX_PROVIDERS | Maximum number of custom OAuth/OIDC providers allowed. Defaults to 0, which means unlimited. |
GOTRUE_CUSTOM_OAUTH_EXTERNAL_URL | Base URL used to build the callback URL for custom providers. Defaults to the value of API_EXTERNAL_URL. |
To change any of these, add the variable to the auth service in your docker-compose.yml:
auth: environment: # ... existing variables ... GOTRUE_CUSTOM_OAUTH_MAX_PROVIDERS: 5Then recreate the Auth service for the change to take effect:
sh run.sh recreate authCreate a provider#
Use the Auth admin API to create providers. You need your project's secret key for authentication.
The JavaScript examples use a supabase-js client created with the secret key (SUPABASE_SECRET_KEY), which must only run in trusted server-side environments.
import { } from '@supabase/supabase-js'const = ('http://<your-domain>', 'your-supabase-secret-key')The Auth service fetches and validates the provider's endpoints when you create the provider, so the request fails if the issuer or endpoint URLs can't be reached.
OAuth 2.0 provider#
Use an OAuth 2.0 provider when your identity provider does not support OpenID Connect discovery. You must supply the authorization, token, and userinfo endpoint URLs explicitly.
const { data, error } = await supabase.auth.admin.customProviders.createProvider({ provider_type: 'oauth2', identifier: 'custom:my-oauth-provider', name: 'My OAuth Provider', client_id: 'your-client-id', client_secret: 'your-client-secret', authorization_url: 'https://provider.example.com/oauth/authorize', token_url: 'https://provider.example.com/oauth/token', userinfo_url: 'https://provider.example.com/oauth/userinfo', scopes: ['profile', 'email'],})OIDC provider#
Use an OIDC provider when your identity provider supports OpenID Connect. Supply the issuer URL and the discovery document, JWKS, and endpoints are resolved automatically.
const { data, error } = await supabase.auth.admin.customProviders.createProvider({ provider_type: 'oidc', identifier: 'custom:my-oidc-provider', name: 'My OIDC Provider', client_id: 'your-client-id', client_secret: 'your-client-secret', issuer: 'https://auth.example.com', scopes: ['openid', 'profile', 'email'],})OIDC providers have the following automatic behavior:
- The discovery document is fetched from
{issuer}/.well-known/openid-configuration, or fromdiscovery_urlwhen it is set. - The
openidscope is always included. It is automatically added if missing from thescopesarray. - ID tokens are verified against the provider's JWKS, which is fetched from the discovery document's
jwks_uri.
Verify the provider#
List the configured providers to confirm the new provider was saved:
curl "http://<your-domain>/auth/v1/admin/custom-providers" \ -H "apikey: your-supabase-secret-key"The response includes the provider you created. Users can now sign in with it from your app:
const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'custom:my-oidc-provider',})Example: Telegram#
The following steps walk through setting up Telegram sign-in with Supabase Auth.
Telegram sign-in fails on Supabase Auth versions before v2.196.0 because of an unsupported signing algorithm issue.
Step 1: Create a Telegram bot#
- Open @BotFather on Telegram and send the command
/newbot. - Follow the prompts to choose a name and username for your bot.
Step 2: Register the redirect URL#
-
Open the @BotFather mini app on Telegram and select the bot you created in the previous step.
-
Select Login Widget to open the page where you configure redirect URIs.
-
If you don't see OIDC settings, click on "Switch to OpenID Connect Login."
-
Click Add a Redirect URI and enter your self-hosted Supabase callback URL:
https://<your-domain>/auth/v1/callback
This screen also shows your Client ID and Client Secret. You use them to create the provider in the next step.
Step 3: Create the Telegram provider#
Copy the Client ID and Client Secret from the Login Widget screen, then use them to create a custom Telegram OAuth provider:
const { data, error } = await supabase.auth.admin.customProviders.createProvider({ provider_type: 'oidc', identifier: 'custom:telegram', name: 'Telegram', client_id: 'your-client-id', client_secret: 'your-client-secret', issuer: 'https://oauth.telegram.org', scopes: ['openid', 'profile'], email_optional: true,})A few notes on these fields:
identifier: can be any value that starts withcustom:and follows the identifier rules.name: can be any value you want.email_optional: must betruebecause Telegram doesn't return an email address.
The profile scope returns the user's name and picture. To also receive the phone number, add the phone scope. For the full list of available scopes, see the Telegram Login docs.
Step 4: Sign in with Telegram#
With the provider configured, you can now sign in with Telegram from your app:
const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'custom:telegram',})Step 5: Test the sign-in flow#
Only the Supabase callback URL needs to be public and served over HTTPS, because that is where the identity provider redirects. The test page itself is a static HTML file that can run anywhere your browser can reach, including your local machine.
- Save the code below to
index.html. - Set
SITE_URLin your self-hosted Supabase.envfile to the URL where the page runs, for examplehttp://localhost:3000, and recreate the Auth service withsh run.sh recreate auth. To allow more than one URL, add the others toADDITIONAL_REDIRECT_URLS. - Start a simple HTTP server via
python -m http.server 3000to serveindex.html. - Open your browser and go to
http://localhost:3000.
<!doctype html><html> <body> <h1>Supabase custom provider test</h1> <button id="loginBtn">Sign in with Telegram</button> <pre id="result"></pre> <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script> <script> document.addEventListener('DOMContentLoaded', function () { const SUPABASE_URL = 'https://<your-domain>' const SUPABASE_PUBLISHABLE_KEY = 'your-supabase-publishable-key' const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY) document.getElementById('loginBtn').addEventListener('click', async () => { const { error } = await supabase.auth.signInWithOAuth({ provider: 'custom:telegram', }) if (error) { document.getElementById('result').textContent = JSON.stringify(error, null, 2) } }) supabase.auth.onAuthStateChange((_event, session) => { if (session) { document.getElementById('result').textContent = JSON.stringify(session.user, null, 2) } }) }) </script> </body></html>Clicking the button sends the browser to your Auth service, which redirects to Telegram. After you confirm the sign-in in the Telegram app, Telegram sends the browser back to https://<your-domain>/auth/v1/callback. The Auth service completes the exchange and redirects to SITE_URL, where supabase-js picks up the session and the page prints the signed-in user.
Telegram doesn't return an email address, so email is empty in the user object. The profile claims that Telegram returns, name, given_name, family_name, and picture, are in user_metadata together with the ID token claims, and app_metadata.provider is custom:telegram. The sub claim is the stable identifier that Auth uses to match the user on later sign-ins.
To test a different custom provider, change the provider value to its identifier.
Manage providers#
Use the admin API to list, update, and delete custom providers. Self-hosted Studio doesn't include a UI for them. The examples use custom:my-provider as the identifier. Replace it with the identifier of your provider.
List providers#
// List all custom providersconst { data, error } = await supabase.auth.admin.customProviders.listProviders()// Filter by provider typeconst { data, error } = await supabase.auth.admin.customProviders.listProviders({ type: 'oidc',})Update a provider#
Send only the fields you want to change. Fields you leave out keep their current values, so you can rotate a client secret by sending client_secret alone. The provider_type and identifier fields are fixed when the provider is created and can't be updated.
const { data, error } = await supabase.auth.admin.customProviders.updateProvider( 'custom:my-provider', { name: 'Updated Provider Name', scopes: ['openid', 'profile', 'email'], })Delete a provider#
const { data, error } = await supabase.auth.admin.customProviders.deleteProvider('custom:my-provider')For PKCE, authorization parameters, and OIDC-specific options, see Advanced configuration.