Repository navigation
Discord #737
Description
Clerk + Discord OAuth is a great fit for this use case. When users sign up/sign in via Discord, Clerk automatically creates (or links) a SaaS user account for them. This is standard social login behavior in Clerk—no extra "upon joining" logic needed for basic auth.
1. Enable Discord in Clerk (Quick Setup)
Development Instance (easiest for testing)
- Go to your Clerk Dashboard → SSO connections.
- Click Add connection → For all users → Select Discord.
- Enable it (Clerk uses shared credentials—no extra config needed).
Production Instance
-
In Clerk Dashboard → SSO connections → Add Discord connection.
-
Toggle on Enable for sign-up and sign-in and Use custom credentials.
-
Copy the Redirect URI shown (e.g.,
https://your-domain.accounts.dev/sign-inor similar for production). -
Create Discord OAuth App:
- Go to Discord Developer Portal.
- New Application → Go to OAuth2 tab.
- Add the Redirect URI from Clerk under Redirects.
- Copy Client ID and Client Secret (reset secret if needed).
-
Paste Client ID + Secret back into Clerk and save.
Test via your app's Account Portal (sign-in page) or <SignIn /> component.
2. Frontend Integration (Next.js / React example)
Assuming you already have Clerk set up:
import { SignIn } from "@clerk/nextjs";
export default function Page() {
return <SignIn path="/sign-in" routing="path" signUpUrl="/sign-up" />;
}Discord button appears automatically once enabled. Users signing in with Discord become SaaS users in Clerk (with externalAccounts linked to their Discord profile).
Clerk handles:
- Sign-up / sign-in equivalence (new users auto-created).
- User metadata (name, email, avatar from Discord).
- Session management, JWTs, etc.
3. Backend / User Management
- Use Clerk's Backend API or SDK to fetch user details:
clerkClient.users.getUser(userId). - Access Discord-linked data via
user.externalAccounts. - Webhooks (recommended for SaaS): Listen to
user.created,user.updatedto sync to your database (e.g., create a profile, assign default role, send welcome email).
4. Advanced: Discord Guild/Member Features
If you want to:
- Check if the user is in your Discord server (guild).
- Sync roles, grant access on join, or manage members.
After auth:
- Get the OAuth access token from Clerk:
const { getToken } = useAuth();then request Discord token with appropriate scopes. - Use Discord API endpoints (
/users/@me/guilds, etc.) with the token.
Scopes (add in Clerk when using custom credentials):
- Default:
identify email - Extra for guilds:
guilds(list servers),guilds.members.read(member details).
Example flow from community projects: After Clerk sign-in, use server actions to call Discord API for role management.
Bot Integration Tip: For automatic role assignment on server join, combine this with a Discord bot that listens for member joins and checks against your Clerk user database (via Discord ID).
Common Gotchas
- Redirect URI mismatches → Exact match required (including protocol, no trailing slashes issues). Use Clerk's provided one.
- Production vs Dev: Always use custom credentials in prod.
- Rate limits / permissions: Be careful with Discord API calls.
- Existing users: Allow linking Discord in user profile if they signed up another way.
Next Steps / Resources
- Official: Clerk Discord Guide.
- Clerk + Next.js social connections overview.
- For full Discord member/role management examples, check tutorials using Clerk + Discord API (e.g., Brian Morrison's projects).
If you share more details (tech stack like Next.js, what "upon joining" specifically means—e.g., auto-role, server invite, paid gating), I can give more targeted code/webhook examples or help debug!
Activity
ngoiyaeric commentedon Jul 14, 2026
@coderabbitai plan
coderabbitai commentedon Jul 14, 2026
Coding Plan
Summary
guildMemberAddand automatically DMs joiners a static link to a dedicated/auth/discordroute, gracefully handling blocked/closed DMs; Discord never appears on the app's/sign-in,/sign-up, or modal./auth/discordroute auto-triggers Clerk'soauth_discordstrategy viaauthenticateWithRedirectwith an explicit post-auth destination; a/sso-callbackroute completes sign-in. Discord IS configured as a Clerk social connection but hidden from the default UI, so account creation/sign-in happens the standard Clerk-OAuth way.providerUserIdon ClerkexternalAccounts; a per-join token can be threaded through the flow (documented additive path) if stronger anti-abuse/binding is required.externalAccounts(extending the existing webhook/resolveClerkUserToDbUsersync), optionally persistingdiscord_id/discord_usernameand mirroring to Supabase; the bot assigns guild roles using its bot token, optionally gated by a read-only bot→app status check.Design Choices
Design Choice 1: How does the bot direct joiners to authenticate, and how is the bot-originated join context correlated with the OAuth result?
Options Considered:
${APP_BASE_URL}/auth/discord) that itself auto-triggers the Discord OAuth flow. Correlation between the join and the resulting user is established implicitly by Clerk: because auth uses theoauth_discordstrategy, the authenticated Clerk user'sexternalAccounts[provider==='discord'].providerUserIdIS the same Discord user ID the bot saw inguildMemberAdd. No separate join token is minted; the Discord ID is the shared correlation key.redirectUrl/redirectUrlCompletequery param or Clerk sign-in metadata), and validate/consume it server-side on the callback to bind the join event to the authenticated user and add anti-abuse (link cannot be forwarded/replayed, expires quickly).Chosen Option: 1
Rationale: A static link with Discord-ID-based correlation requires no token table, no bot→app minting API, and no custom binding logic — Clerk's
oauth_discordflow guarantees the authenticated user carries the same DiscordproviderUserIdthe bot already holds, which is sufficient for downstream role assignment. Because the link only initiates a standard OAuth consent (which itself requires the user to authenticate with Discord), the abuse surface is low: a forwarded link merely lets another Discord user start their own legitimate OAuth flow. If stronger anti-abuse or explicit join-to-user binding is later required, Task 2.2/2.3 document exactly where a per-join token would be threaded through (redirectUrlstate → validated/consumed in the callback), so Option 2 remains a clean, additive upgrade rather than a rewrite.Design Choice 2: Should Discord be configured as a Clerk social connection at all, given it must not appear on the default sign-in UI?
Options Considered:
<SignIn />/<SignUp />UI or modal; it is invoked exclusively through a customsignIn.authenticateWithRedirect({ strategy: 'oauth_discord', ... })call.Chosen Option: 1
Rationale: Discord absolutely IS a Clerk social connection — that is what lets Clerk natively create/link the user and populate
externalAccounts. The only thing suppressed is its visibility on the default UI surfaces. Per Clerk's docs, disabling "Enable for sign-up and sign-in" makes the provider link-only in the prebuilt components while still allowing custom OAuth flows viaauthenticateWithRedirect, which cleanly satisfies "configured in Clerk, but never shown on the default surfaces." Option 2 would discard Clerk's native user creation/externalAccountsand is unnecessary.Design Choice 3: Where should Discord identity come from and be persisted?
Options Considered:
externalAccounts(populated automatically by the OAuth flow) and optionally mirror it into theuserstable.discord_idlinking flow driven by the bot/verification token.Chosen Option: 1
Rationale: Because sign-in now happens the standard Clerk-OAuth way, the Discord identity (
provider: 'discord',providerUserId,username,imageUrl,approvedScopes) is already present on the Clerk user'sexternalAccounts; the app reads it from there rather than maintaining a separate linking flow.Design Choice 4: How does the bot perform guild role assignment after a user authenticates?
Options Considered:
guildMemberAddevent (optionally confirming "has this Discord ID authenticated?" via a thin read-only bot→app status call sourced from ClerkexternalAccounts).Chosen Option: 1
Rationale: The bot is already a guild member with the Discord user ID from the join event, so it can assign roles directly; any bot↔app communication is limited to a read-only "is this Discord ID linked/authenticated?" check, not identity linking.
💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: Discord bot deployment & configuration
This phase implements the always-on bot that automatically (on guild join, not via commands) DMs the joiner a link to the dedicated app auth route, and is configured with the correct intents/permissions. The bot performs join detection and messaging only — it does not mint tokens or link identities.
Build and host the always-on Discord bot
Create a persistent bot service that automatically reacts to new members joining the guild and DMs them the auth link.
discord-bot/, or a separate deployable service) using discord.js v14 with the privilegedGuildMembersintent enabled; keep intents minimal (only what is needed forguildMemberAddand role assignment). The bot is fully event-driven — there is NO slash-command/manual trigger; onboarding fires automatically on join.guildMemberAddevent; on every join, automatically DM the new member a static link to the dedicated app auth route (${APP_BASE_URL}/auth/discord) viaGuildMember#send().GuildMember#send()in.catch(), log the failure, and do not crash the bot when a member has DMs disabled, has blocked the bot, or otherwise cannot receive the message (optionally fall back to a public welcome-channel mention if configured).docker-compose.yaml(alongside the existing app/db/qdrant services) or a Fly.io/Railway deployment; document setup (create Discord application + bot in the Discord Developer Portal, obtain the bot token, enable the Members intent, invite the bot to the guild with role-management permission).SCREAMING_SNAKE_CASE/provider-prefix convention:DISCORD_BOT_TOKEN,DISCORD_GUILD_ID, the app base URL used to build the auth link, and (only if the optional role-status check in Phase 3 is implemented)DISCORD_BOT_API_SECRET. Add new secrets to.env.local.example.🤖 Prompt for AI agents
Phase 2: Clerk Discord provider config + dedicated OAuth-trigger route & callback
This phase configures Discord as a Clerk social connection that is hidden from the default sign-in/sign-up UI, and adds the dedicated route that auto-triggers the
oauth_discordflow plus the callback route that completes sign-in and lands the user at the configured post-auth destination. Discord must never appear on/sign-in,/sign-up, or the existing modal.Task 1: Configure Discord as a Clerk social connection hidden from the default UI
Enable Discord OAuth in Clerk (it IS a Clerk connection) without surfacing it on the prebuilt auth components.
identify emailscopes plusguilds/guilds.members.readonly if needed downstream.<SignIn />/<SignUp />components or the existing modal — Discord remains a fully configured Clerk connection but is invoked exclusively via the custom flow in Task 2.2.Task 2: Add the dedicated OAuth-trigger route
Provide the bot-linked route that immediately starts the Discord OAuth flow and defines the post-auth destination.
app/auth/discord/page.tsxas a client component (following the'use client'pattern ofapp/offline/page.tsx) that, on mount, calls Clerk'ssignIn.authenticateWithRedirect({ strategy: 'oauth_discord', redirectUrl: '/sso-callback', redirectUrlComplete: '<post-auth destination>' }).redirectUrlComplete) — default to the app home/dashboard (/), configurable via env/constant so onboarding lands users on the intended landing page after sign-in./auth/discord?jt=...) and is threaded into the OAuth flow viaredirectUrlstate (e.g./sso-callback?jt=...) so the callback can validate/consume it — the static-link default omits this but the route is structured to accept it additively./sign-in,/sign-up, or the modal.app/layout.tsx(ClerkProvider) and requires no middleware matcher change; render a lightweight "Redirecting to Discord…" UI while the redirect is initiated.Task 3: Add the Clerk OAuth callback route and document join-context correlation
Complete the OAuth flow, route the user into the app, and record how the join event correlates to the authenticated user.
app/sso-callback/page.tsxrendering Clerk's<AuthenticateWithRedirectCallback />to finish the OAuth return and establish the session, then land the user at the configuredredirectUrlCompletedestination (Task 2.2).guildMemberAddDiscord user ID and the authenticated Clerk user are correlated via the shared DiscordproviderUserIdonexternalAccounts(populated by the OAuth flow) — no separate join token is required for the default flow.expiresAt/consumedAt, bind the join to the resulting user) before honoringredirectUrlComplete.🤖 Prompt for AI agents
Phase 3: Post-login identity sync, role assignment & visibility
This phase covers everything after a user authenticates via Discord OAuth: reading the Discord identity from Clerk's
externalAccounts, optionally persisting/mirroring it, bot-driven guild role assignment, and optional user-facing visibility. Identity is sourced from Clerk — there is no custom token/DB linking flow.Task 1: Sync Discord identity from Clerk externalAccounts into the app DB
Read the Discord identity Clerk attaches during OAuth and persist it for downstream use.
externalAccount(whereprovider === 'discord'): updateresolveClerkUserToDbUser/syncUserProfileinlib/auth/get-current-user.tsand theuser.created/user.updatedhandler inapp/api/clerk/webhook/route.ts(which today only read email/name/avatar) to also extractproviderUserId(Discord ID),username, andimageUrlfromcurrentUser().externalAccounts/evt.data.external_accounts.usersrow; do NOT implement any custom "link Discord ID to an already-authenticated user" flow — the identity is authoritative from Clerk's OAuth-created user.console.error, best-effort profile refresh) already used in these files.Task 2: Extend the users schema with Discord identity fields
Add persistent, queryable Discord identity columns populated from Clerk externalAccounts.
lib/db/schema.tsuserstable to add adiscordId(discord_id) text column (unique), adiscordUsernamecolumn, and reuse/extend the existingmetadata jsonbfor any variable Discord data (e.g. guild membership/roles).drizzle/migrations/0009_add_discord_account_link.sqlfollowing existing conventions: descriptive header comment,ADD COLUMN IF NOT EXISTS, unique constraint /CREATE INDEX IF NOT EXISTS idx_users_discord_id, and (if RLS applies) mirror the establishedis_clerk_user/clerk_id()policy patterns.externalAccounts, not from any bot-provided linking payload.Task 3: Optionally mirror the Discord identity to the Supabase backend
Keep the Supabase "QCX-BACKEND" in sync if Discord identity is needed there for RLS/Realtime.
sync_clerk_userRPC signature/body (in the same or a follow-on migration) to accept and persistdiscord_id/discord_username, and pass the new values from the existing Clerk webhook (app/api/clerk/webhook/route.ts) sync path.Task 4: Bot-driven guild role assignment & optional onboarding/gating
Grant Discord roles/access to authenticated members, driven by the bot.
GuildMember), keyed off the Discord user ID it already holds fromguildMemberAdd; handle Discord rate limits and missing-permission cases gracefully.app/api/discord/(authenticated with the shared-secretDISCORD_BOT_API_SECRETheader, mirroring the "handler enforces its own check" approach ofapp/api/clerk/webhook/route.ts) that answers whether a given Discord ID exists in theuserstable (sourced from the Clerk-synceddiscord_id). This bot↔app channel is for role/status only — never for identity linking.Task 5: Add a "Linked Accounts / Integrations" settings tab (optional)
Give signed-in users visibility into their Discord link.
components/settings/components/settings.tsx(Tabs.Trigger+Tabs.Content) backed by a new component (modeled onuser-management-form.tsx) that shows Discord link status, guild membership/roles, and (optionally) a disconnect affordance.noStore()read action (modeled onlib/actions/users.tsgetSelectedModel) returning the current user's Discord identity (from the Clerk-synced columns) plus any guild/role data./auth/discordroute, never via a Discord button on the auth surfaces or modal.🤖 Prompt for AI agents
Research
This is a Next.js (App Router) app using Clerk (
@clerk/nextjs@^7.5.13,@clerk/backend@^3.11.0) for auth, wrapped globally viaclerkMiddleware()inmiddleware.tsand<ClerkProvider>inapp/layout.tsx. Authentication is modal-only (<SignInButton mode="modal">) — there are no dedicated/sign-inor/sign-uppages. User identity is bridged into a Drizzle-managed Postgresuserstable (lib/db/schema.ts) via three sync paths: on-demandresolveClerkUserToDbUser(lib/auth/get-current-user.ts), the client-triggeredsyncUseraction (lib/actions/sync-user.ts+components/user-sync.tsx), and the Clerk webhook (app/api/clerk/webhook/route.ts), which also dual-writes to a Supabase "QCX-BACKEND" project via thesync_clerk_userRPC. Today the Clerk sync only readsemailAddresses,firstName,lastName,imageUrl— neverexternalAccounts. Theuserstable has a generic unusedmetadata jsonbcolumn but no Discord/external-account fields. No API route uses a static shared-secret/API-key check — the only inbound external caller (Clerk's webhook) uses svix HMAC verification, a good model for authenticating any bot→app calls. The app has a real containerized/always-on deployment path (Dockerfile, docker-compose, Cloud Run) in addition to Vercel, so a separate persistent bot service is feasible. Third-party API integrations follow a Composio pattern (mapbox_mcp/), settings live in a tabbed UI (components/settings/components/settings.tsx), and migrations are hand-written SQL with the next available number being0009.🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
@coderabbitai <feedback>💬 Have feedback or questions? Drop into our discord!
ngoiyaeric commentedon Jul 14, 2026
@coderabbitai I need the discord joiners to auth onto the app as well but not from the app with discord so not on the front end but from the backend on a different page.
coderabbitai commentedon Jul 14, 2026
🧩 Analysis chain
🏁 Script executed:
Length of output: 240
🏁 Script executed:
Length of output: 2393
@ngoiyaeric, thanks for the clarification — that's a meaningfully different flow than Phase 1 of the earlier plan (which put Discord on the public/sign-in//sign-uppages). Here's what I found and how I'd adjust:Key constraint discovered: This app is deployed as a Next.js app on Vercel (serverless —
next.config.mjs,@vercel/analytics,@vercel/speed-insights). There's currently no Discord bot code, nodiscord.jsdependency, and no always-on worker process in the repo. That matters because:guildMemberAddevent) — Vercel serverless functions can't hold that connection open.So there are two ways to get "joiners" authenticated from the backend, on a page separate from the app's normal sign-in flow:
Self-serve, Vercel-friendly (recommended default): A Discord bot registers a slash command (e.g.
/verify) using the HTTP Interactions endpoint (a newapp/api/discord/interactions/route.ts). When a server member runs it, the backend mints a short-lived one-time token, stores it, and DMs the user a link like/connect/discord?token=.... That dedicated page (not/sign-in, not/sign-up) validates the token server-side and then — instead of rendering Clerk's default<SignIn/>(which would also surface every other enabled social button) — callsuseSignIn().authenticateWithRedirect({ strategy: 'oauth_discord', redirectUrl, redirectUrlComplete })directly, so Discord OAuth only ever runs from this one backend-driven page. On callback, we markdiscordId+ adiscordJoinSource: 'guild-verify'flag using the Phase 2 schema/sync logic already planned.True auto-detection on join (needs new infra): If you want authentication triggered the moment someone joins the guild (no
/verifycommand needed), that requires a small always-on bot process (e.g. on Fly.io/Railway) listening forguildMemberAdd, which then calls back into a new authenticated API route on this app to mint the token and send the DM. This is more infrastructure to stand up and maintain outside Vercel.Before I turn this into a concrete ``, could you confirm:
/connect/discord,/discord/verify)?Once you confirm, I'll produce the full `` covering the new interactions endpoint, token model/migration, the dedicated page, and the Clerk
authenticateWithRedirectwiring — kept fully separate from the existing `/sign-in`/`/sign-up` pages and modal.✏️ Learnings added
💡 For code changes across multiple files, use
@coderabbitaiplan in an issue commentngoiyaeric commentedon Jul 14, 2026
@coderabbitai plan option 2 for an always on discord bot that prompt new joiners for their auth into a separate page on the app
coderabbitai commentedon Jul 14, 2026
I've updated the plan above based on your feedback. Ready to proceed when you are!
ngoiyaeric commentedon Jul 16, 2026
@coderabbitai Yes. This is a common pattern with Clerk.
You can configure Discord as an authentication provider but keep it hidden from your public sign-in page. Users arriving from your Discord server (or Discord OAuth flow) can still authenticate via a dedicated route.
Architecture
Option 1 (Recommended): Separate Discord Sign-In Route
Instead of displaying Discord on your main Clerk component:
show only:
Then create
where you immediately start the Discord OAuth flow.
For example:
Anyone visiting
is immediately sent through Discord authentication.
Option 2: Hide Discord from Clerk UI
If you're using Clerk's
<SignIn />component, configure it so Discord isn't one of the displayed social providers.For example:
Or build a custom sign-in page that only renders the providers you want. Discord remains enabled in the Clerk Dashboard, but users won't see a Discord button on your main page.
Discord Invite Flow
Suppose someone joins your Discord community.
This gives Discord members a seamless entry into your application without exposing Discord login to everyone else.
If you need to identify Discord members
After Clerk authenticates them, you'll have access to their external account information, including the OAuth provider. You can check whether the session came from Discord:
You can then:
For your QCX app, this approach keeps the main authentication experience clean while allowing a frictionless "Join Discord → Open App → Automatically Sign In" flow for community members. It also leaves room to add Discord-specific onboarding or permissions without affecting other sign-in methods.
coderabbitai commentedon Jul 16, 2026
I've updated the plan above based on your feedback. Ready to proceed when you are!
coderabbitai commentedon Jul 16, 2026
I've updated the plan above based on your feedback. Ready to proceed when you are!