Single Sign-On (SSO)

Discord

Add Discord sign-in to VitNode - create an application, register the redirect URL and wire up the Discord SSO adapter in your API config.

Discord is the quickest of the three to set up: no consent screen review, no app modes, no verification. Create an application, add one redirect URL, copy two values.

Quick start

If you already have a client ID and secret, this is the entire integration.

.env
DISCORD_CLIENT_ID=1234567890123456789
DISCORD_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
src/vitnode.api.config.ts
import { DiscordSSOApiPlugin } from '@vitnode/core/api/adapters/sso/discord'
import { buildApiConfig } from '@vitnode/core/vitnode.config'

export const vitNodeApiConfig = buildApiConfig({
  plugins: [],
  authorization: {
    ssoAdapters: [
      DiscordSSOApiPlugin({
        clientId: process.env.DISCORD_CLIENT_ID,
        clientSecret: process.env.DISCORD_CLIENT_SECRET,
      }),
    ],
  },
})

Restart the API and a Discord button appears on /login.

Set up the application

Sign in to the Discord Developer Portal

Go to the Discord Developer Portal and sign in with the Discord account that should own the application.

Create a new application

Press New Application, give it the name visitors will see on the authorization screen, and accept the terms.

Add the redirect URL

Open OAuth2 in the left navigation and, under Redirects, press Add Redirect. Paste the URI for the origin you are running and save.

VitNode builds this URI from NEXT_PUBLIC_WEB_URL, so it has to match exactly - no trailing slash, no locale prefix.

EnvironmentRedirect URL
Development (NEXT_PUBLIC_WEB_URL unset)http://localhost:3000/login/sso/discord
Productionhttps://your-domain.com/login/sso/discord

Add both entries so one application covers local development and your live site.

Invalid OAuth2 redirect_uri

Discord rejects the authorization request outright if the URL is not in this list, before the visitor sees anything. If pressing the button lands you on a Discord error page rather than an authorization prompt, this is why.

Copy the client ID and secret

Still on the OAuth2 page, the Client ID and Client Secret are in the client information block at the top. Press Reset Secret if the secret was never revealed or you have lost it - Discord shows it once.

Set the environment variables

Server-side variables, so no NEXT_PUBLIC_ prefix - that prefix is what marks a value for the browser bundle, and a client secret in the browser bundle is a client secret you have published.

.env
DISCORD_CLIENT_ID=1234567890123456789
DISCORD_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Register the adapter

Add DiscordSSOApiPlugin to authorization.ssoAdapters. It is a plain factory function, not a class - there is no new.

src/vitnode.api.config.ts
import { DiscordSSOApiPlugin } from '@vitnode/core/api/adapters/sso/discord'
import { buildApiConfig } from '@vitnode/core/vitnode.config'

export const vitNodeApiConfig = buildApiConfig({
  plugins: [],
  authorization: {
    ssoAdapters: [
      DiscordSSOApiPlugin({
        clientId: process.env.DISCORD_CLIENT_ID,
        clientSecret: process.env.DISCORD_CLIENT_SECRET,
      }),
    ],
  },
})

The redirect URI is computed when this file is evaluated, so restart the API rather than relying on a hot reload:

bun dev
pnpm dev
npm run dev

Verify it works

Ask the API what it thinks it supports:

curl http://localhost:3000/api/@vitnode/core/middleware
# {"isEmail":false,"sso":[{"id":"discord","name":"Discord"}]}

Then open /login and press Discord. You should see Discord's "connect to" authorization prompt listing your username and email, get bounced back to /login/sso/discord?code=...&state=..., and land on the front page signed in.

What the adapter asks Discord for

Useful when you are debugging an authorization prompt that lists the wrong permissions.

SettingValue
Authorize URLhttps://discord.com/oauth2/authorize
Token URLhttps://discord.com/api/oauth2/token
Profile URLhttps://discord.com/api/users/@me
Scopesidentify email
Provider iddiscord
Fields readid, email, username

Both scopes are required: identify for the id and username, email for the address the account is created with.

Gotchas

An account with no email cannot sign in

The adapter requires email in Discord's profile response and answers 400 when it is absent - which is what happens if the email scope is dropped, or if the Discord account genuinely has no address on it. The visitor sees the generic error screen.

Discord's email verification flag is not checked

Unlike the Google adapter, which rejects an unverified address, this one takes Discord's email at face value and never reads Discord's verified field. If you need that guarantee, copy the adapter into your own project and add the check - see Custom adapter.

A missing key fails on click, not on boot

A DiscordSSOApiPlugin whose clientId is undefined registers happily - the button renders and the provider is listed - and throws Missing Discord client ID the moment somebody presses it. A button that only raises an error toast is an environment problem, not a portal problem.

Next