Discord Verification Frontend Flow¶
See also: Discord Integration Configuration for backend configuration and setup.
Overview¶
This guide describes how the Discord verification frontend flow works in the shipped implementation.
There is no dedicated SvelteKit verification route. The verification page itself is HTML served by the backend (FastEndpoints), and the only piece that lives in the SvelteKit app (src/Apps/foundry-web) is the login page, which honours a redirect query parameter after a successful login. The two pieces are wired together through the BallrUrl and Discord:Verification:BaseUrl configuration keys.
The pieces involved are:
- Backend verification page —
GET /discord/verify?token=..., served byDiscordVerificationPageEndpoint. - Backend completion endpoint —
POST /api/discord/verify/complete, served byCompleteDiscordVerificationEndpoint. - SvelteKit login page —
src/Apps/foundry-web/src/routes/login/+page.svelte, which reads theredirectquery parameter and navigates back to it after login.
Verification Flow¶
sequenceDiagram
participant User
participant Discord
participant VerifyPage as Backend /discord/verify
participant Login as SvelteKit /login
participant Complete as Backend /api/discord/verify/complete
User->>Discord: /verify command
Discord->>User: Embed with "Verify" button linking to {Discord:Verification:BaseUrl}/discord/verify?token=...
User->>VerifyPage: Open verification link
VerifyPage->>VerifyPage: Validate HMAC token (DiscordTokenService)
VerifyPage->>Complete: POST token (checkAuthentication on page load)
Complete-->>VerifyPage: 401 if not logged in
VerifyPage->>User: Show "Login to Ballr" button → {BallrUrl}/login?redirect=...
User->>Login: Open login page with redirect param
User->>Login: Submit credentials (authStore.login)
Login->>User: goto(redirectUrl) → back to {BallrUrl}/discord/verify?token=...
User->>VerifyPage: Reopen verification page (now authenticated)
VerifyPage->>Complete: POST token (now carries auth)
Complete->>Complete: Link Discord account + assign verified role
Complete-->>User: success → page shows confirmation and closes
Backend Verification Page¶
The verification page is rendered entirely by the backend. DiscordVerificationPageEndpoint registers the anonymous route GET /discord/verify, validates the HMAC token via DiscordTokenService, and returns an HTML page (or an error page if the token is invalid or expired).
The page does two things in the browser:
- On load it calls
POST /api/discord/verify/completewith the token. If the response is401the user is not authenticated, so the "Login to Ballr" button is shown. If the user is already authenticated the call links the account immediately and shows a success message. - The "Login to Ballr" button links to the SvelteKit login page with a
redirectparameter pointing back at the verification page:
This link is built in GenerateVerificationPage, using the BallrUrl configuration key (default https://ballr.live) and Uri.EscapeDataString for the redirect value.
The completion endpoint, CompleteDiscordVerificationEndpoint, requires authentication (AuthorizationPolicyDefinition.RequireMember, i.e. a Clerk JWT). It validates the token, resolves the current user, performs an idempotency check via DiscordUserLookupQuery, executes LinkDiscordAccountCommand, and assigns the verified Discord role.
SvelteKit Login Page¶
The login page is the only frontend component in this flow. It lives at src/Apps/foundry-web/src/routes/login/+page.svelte.
It reads the redirect query parameter, defaulting to /:
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { authStore } from '$lib/stores/auth.svelte';
// Get redirect URL from query params
const redirectUrl = $derived($page.url.searchParams.get('redirect') || '/');
If the user is already authenticated when the page mounts, it navigates straight to the redirect target:
import { onMount } from 'svelte';
onMount(() => {
if (authStore.isAuthenticated) {
goto(redirectUrl);
}
});
After a successful login it does the same:
const result = await authStore.login(email, password);
if (result.success) {
// Redirect to the original page or home
goto(redirectUrl);
} else {
error = result.error || 'Login failed. Please try again.';
isLoading = false;
}
The redirect parameter is a generic "return to where you came from" mechanism shared with the rest of the app: the root layout src/Apps/foundry-web/src/routes/+layout.svelte sends unauthenticated users on protected routes to /login?redirect=<currentPath>. The Discord verification flow reuses the same parameter; the only difference is that the redirect target is the backend-served /discord/verify?token=... page rather than an in-app route.
The login page does not special-case Discord. It does not display a Discord-specific notice, and it does not contain any redirect-domain allowlist. The redirect value sent by the verification page is a same-origin path under BallrUrl, so goto returns the now-authenticated browser to the backend verification page, where the on-load POST /api/discord/verify/complete call now carries the user's session and completes the link.
Configuration¶
The flow relies on two configuration keys:
| Configuration Key | Used by | Purpose |
|---|---|---|
BallrUrl |
DiscordVerificationPageEndpoint (GenerateVerificationPage) |
Base URL of the SvelteKit app; used to build the /login?redirect=... link. Defaults to https://ballr.live when unset. |
Discord:Verification:BaseUrl |
InitiateDiscordVerificationEndpoint and DiscordBotService |
Base URL used to build the /discord/verify?token=... link delivered to the user. |
In local development, the Aspire host sets both keys. BallrUrl is read from configuration (http://localhost:3000 in appsettings.json) and Discord__Verification__BaseUrl is wired to the API's https endpoint. See ServiceCollectionExtension.
For the full set of Discord configuration keys (bot token, HMAC secret, token expiry, etc.) see Discord Integration Configuration.
Testing the Flow¶
End-to-end (local)¶
- Start the backend stack via the Aspire host.
- Start the SvelteKit app (
pnpm web:dev), reachable athttp://localhost:3000(matchingBallrUrl). - Use the Discord bot
/verifycommand to receive a verification link. - Open the link. The backend serves the verification page; because you are not yet authenticated, the "Login to Ballr" button is shown.
- Click it. You land on
/login?redirect=.... Sign in. - After login,
gotoreturns you to the verification page, which completes the link and assigns the verified role.
Manual login-redirect check¶
You can exercise the login redirect directly by visiting a URL with a redirect parameter, for example:
After a successful login the page navigates to the decoded redirect target.
Troubleshooting¶
- Verification page shows "Invalid or expired verification link" — the HMAC token failed validation in
DiscordTokenService(wrongDiscord:Verification:HmacSecret, tampered token, or expired). Request a fresh link. - "Login to Ballr" link points at the wrong host — check
BallrUrl; it is the base of the/loginlink generated by the verification page. - Login succeeds but verification does not complete — confirm the
redirecttarget resolves back to{BallrUrl}/discord/verify?token=...on the same origin, and that the browser session carries the Clerk JWT required byPOST /api/discord/verify/complete.