Authentication & Roles
Overview
The API uses Discord OAuth2 for authentication. On success, the server issues a session cookie. All protected routes validate this cookie on every request.
A separate key-based scheme exists for the mobile check-in app.
OAuth2 Flow
Client API Discord
│ │ │
│── GET /auth/callback?code ──▶│ │
│ │── exchange code ────────────▶│
│ │◀─ access token ─────────────│
│ │── GET /users/@me ───────────▶│
│ │◀─ Discord user info ─────────│
│ │ │
│ │ (upsert user + session) │
│ │ │
│◀─ Set-Cookie: sh_session_id ─│ │
│◀─ 302 → CLIENT_URL ──────────│ │
- The frontend initiates the OAuth2 flow by redirecting the user to Discord with a
statenonce stored in thesh_auth_noncecookie. - Discord redirects back to
/auth/callbackwith acodeandstate. - The API validates the nonce, exchanges the code for a Discord access token, and fetches the user's Discord profile.
- If the user is new, an
auth.usersrecord andauth.accountsrecord are created in a transaction. Otherwise, a new session is created for the existing user. - The session ID is set as the
sh_session_idcookie and the user is redirected to the frontend.
Session Validation
Every request to a protected route goes through RequireAuth middleware:
- Reads the
sh_session_idcookie. - Looks up the session in
auth.sessions(must not be expired). - Fetches the associated user record.
- Attaches a
UserContextto the request context.
Rolling expiration: If the session has not been used in the past 24 hours, its expiration is extended by 30 days and the cookie is refreshed.
UserContext fields
| Field | Type | Description |
|---|---|---|
UserID |
UUID | Unique user identifier |
Email |
*string |
Primary email from Discord |
PreferredEmail |
*string |
User-set preferred email |
Name |
string | Display name |
Onboarded |
bool | Whether onboarding is complete |
Image |
*string |
Profile image URL |
Role |
AuthUserRole |
Platform role (user or superuser) |
EmailConsent |
bool | Whether the user opted into emails |
Platform Roles
Two platform-level roles are defined in auth_user_role:
| Role | Description |
|---|---|
user |
Default role for all registered users |
superuser |
Full access; bypasses all role checks |
Platform roles are enforced by RequirePlatformRole(roles) middleware. Superusers bypass this check unconditionally.
Event Roles
Users can have a role within a specific event, stored in event_roles:
| Role | Description |
|---|---|
admin |
Full event management (create/delete/assign roles, release decisions) |
staff |
Event operations (check-in, review applications, manage redeemables) |
attendee |
Accepted attendee |
applicant |
Has submitted an application |
Event roles are enforced by RequireEventRole(roles) middleware, which fetches the user's role for the event from the URL path. Superusers bypass event role checks.
Mobile Authentication
The mobile check-in app uses a static key instead of session cookies:
Routes under /mobile require this header. The key is configured via the MOBILE_AUTH_KEY environment variable (not in .env.dev.example — request it from the team).
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/auth/callback |
None | OAuth2 callback |
GET |
/auth/me |
Session | Get current user |
POST |
/auth/logout |
Session | Invalidate session |