If you're building a gaming app, a community tool, or anything whose users already live in Discord servers, Discord login is the lowest-friction auth you can offer. On Supabase Cloud it's a two-minute dashboard toggle. On self-hosted Supabase there is no toggle — the Auth service (GoTrue) only reads OAuth configuration from environment variables, and getting Discord working means editing your .env, restarting containers, and debugging redirect mismatches by hand.
This guide walks through the whole thing: creating the Discord application, wiring up the GoTrue environment variables, handling redirect URLs correctly, and fixing the errors that fill the GitHub discussions on this topic. If you haven't set up any social login yet, start with our general guide to setting up OAuth providers for self-hosted Supabase first — this post assumes your stack is running and reachable over HTTPS.
Step 1: Create a Discord Application
Head to the Discord Developer Portal and click New Application. Name it after your product — this name is what users see on the consent screen, so don't call it "test-app-3".
Under OAuth2 in the left sidebar you'll find the two values you need:
- Client ID — public identifier for your app
- Client Secret — click Reset Secret to generate one; copy it immediately
Then add your redirect URI. This is the single most common failure point, so get it exactly right:
https://api.yourdomain.com/auth/v1/callback
Three rules apply:
- The host must match your
API_EXTERNAL_URL— the public URL of your Kong gateway, not your app's frontend URL and notlocalhost. - The path is always
/auth/v1/callback. Not/auth/callback, not/auth/v1/callback/. - It must be HTTPS. Discord rejects plain HTTP redirect URIs for anything other than localhost, which is one more reason to sort out custom domains and SSL for your instance before configuring providers.
Discord matches redirect URIs character-for-character. A trailing slash or an http:// where you meant https:// produces the infamous Invalid OAuth2 redirect_uri screen.
Step 2: Configure GoTrue Environment Variables
Self-hosted GoTrue configures every OAuth provider through the same four-variable pattern. For Discord, add these to the .env file that feeds your Docker Compose stack:
GOTRUE_EXTERNAL_DISCORD_ENABLED=true GOTRUE_EXTERNAL_DISCORD_CLIENT_ID=your-discord-client-id GOTRUE_EXTERNAL_DISCORD_SECRET=your-discord-client-secret GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI=https://api.yourdomain.com/auth/v1/callback
Then make sure your docker-compose.yml actually passes them into the auth service. The stock Supabase compose file only forwards variables it knows about, so if you've trimmed it down, add them explicitly:
auth:
environment:
GOTRUE_EXTERNAL_DISCORD_ENABLED: ${GOTRUE_EXTERNAL_DISCORD_ENABLED}
GOTRUE_EXTERNAL_DISCORD_CLIENT_ID: ${GOTRUE_EXTERNAL_DISCORD_CLIENT_ID}
GOTRUE_EXTERNAL_DISCORD_SECRET: ${GOTRUE_EXTERNAL_DISCORD_SECRET}
GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI: ${GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI}
Restart the auth container to pick up the changes:
docker compose up -d auth
A quick sanity check — this endpoint should redirect you to Discord's consent screen:
https://api.yourdomain.com/auth/v1/authorize?provider=discord
If it returns {"error":"unsupported provider"} instead, the environment variables didn't reach the container. docker exec supabase-auth env | grep DISCORD will tell you what GoTrue actually sees, which is usually more informative than what your .env says.
Step 3: Trigger the Flow From Your App
On the client side, Discord works like every other Supabase provider:
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'discord',
options: {
redirectTo: 'https://app.yourdomain.com/auth/callback',
},
});
The redirectTo here is where users land after GoTrue finishes the exchange — your application, not the API gateway. For it to work, that URL must be allowed by your GOTRUE_URI_ALLOW_LIST (and GOTRUE_SITE_URL acts as the fallback). If users complete Discord's consent screen and then get dumped onto your Kong URL or the Studio dashboard instead of your app, this allow-list is the culprit — a problem common enough that we wrote a dedicated guide to redirect URLs and Site URL configuration.
Requesting Extra Scopes
By default Supabase requests Discord's identify and email scopes. If you want to check which servers a user belongs to — the classic gated-community use case — add the guilds scope:
await supabase.auth.signInWithOAuth({
provider: 'discord',
options: {
scopes: 'identify email guilds',
redirectTo: 'https://app.yourdomain.com/auth/callback',
},
});
The provider access token comes back in the session as provider_token, and you can call Discord's /users/@me/guilds endpoint with it. One honest caveat: Supabase doesn't store or refresh provider tokens for you. The provider_token is only reliably present right after sign-in, so if you need ongoing guild checks, capture it then and handle refresh yourself.
The Errors Everyone Hits
"Invalid OAuth2 redirect_uri" on Discord's side. The URI in the Developer Portal doesn't exactly match what GoTrue sent. Compare GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI against the portal entry character by character — scheme, host, path, no trailing slash.
Email conflicts with existing accounts. Discord accounts can share an email with a user who already signed up via magic link or Google. Supabase links them automatically only when the provider verifies the email; otherwise the user gets an "identity already exists" style error. Decide up front whether to enable manual identity linking.
Users without an email. Rare, but Discord accounts registered by phone may not return an email, and GoTrue will reject the sign-in. Handle that error in your UI rather than showing a generic failure.
Works locally, fails in production. Almost always API_EXTERNAL_URL still pointing at localhost:8000, which GoTrue then uses to build its callback URL. Every URL in the auth flow must be your real public domain.
Doing This Without Hand-Editing Env Files
None of the above is difficult the first time. It becomes tedious the fifth time — every new provider, every rotated secret, every new project means editing .env, syncing compose files, restarting containers, and re-testing the flow. Multiply that across the several instances most self-hosters end up running and it's a real maintenance line item.
This is one of the gaps Supascale was built to close. It gives your self-hosted instance the provider configuration UI that Supabase Cloud has: pick Discord from the auth providers panel, paste the client ID and secret, and it writes the GoTrue configuration and restarts the right service for you — same for Google, GitHub, and the rest. The redirect URI is displayed ready to copy into the Discord portal, which quietly eliminates the most common typo in this whole flow. It's a one-time license covering unlimited projects, so the tenth provider on the fifth instance costs the same as the first.
One related note: your Discord consent screen shows your API domain to users. If that's a raw VPS hostname it looks unprofessional — the fix is the same one described in our guide to cleaning up OAuth branding, and it applies to Discord just as much as Google.
Wrapping Up
Discord OAuth on self-hosted Supabase comes down to four environment variables and one exactly-matching redirect URI. The setup itself is fifteen minutes; the debugging, when something's off, is where the hours go — and it's nearly always one of three things: a redirect URI mismatch in the Developer Portal, env vars that never reached the auth container, or an allow-list that doesn't include your app's callback URL. Check those three before anything else.
Once it works, resist the urge to never touch it again: rotate the client secret periodically, and if you add scopes later, remember users need to re-consent before the new scopes appear in their tokens.
