Authentication

Grids uses Discord OAuth for user authentication, with JWT tokens for API access and a device-code flow for linking the game client to a web account.

Discord OAuth Flow

1. Initiate Login

GET /auth/discord

Redirects the user to Discord's authorization page. After approval, Discord redirects back to:

GET /auth/discord/callback?code=...

2. Callback

The callback exchanges the authorization code for a Discord access token, fetches the user profile, and creates or updates the user in the D1 database.

Response: Returns a JWT token and redirects to the web app.

3. JWT Token

Tokens use HS256 signing with a 24-hour expiry. The payload contains:

{
  "sub": "user_id",
  "discord_id": "123456789",
  "display_name": "PlayerName",
  "iat": 1712000000,
  "exp": 1712086400
}

Discord Membership Gate

Playing Grids requires being a member of the official Grids Discord server — this acts as the platform allow-list. Membership is checked server-side using a Discord bot token (GET /guilds/{id}/members/{discord_id}), so no extra OAuth scope is requested from players.

Enforcement happens at two points:

When the gate blocks a world entry, the API responds:

{
  "error": "discord_membership_required",
  "invite_url": "https://discord.gg/..."
}

The check fails open on transient Discord errors (network, rate limit, 5xx) so an outage never locks out legitimate players, and fails closed only on a definitive "not a member" (Discord 404). The gate is disabled automatically if DISCORD_BOT_TOKEN / DISCORD_GUILD_ID are not configured.

Mid-session enforcement

Dedicated servers re-check membership for connected players on a timer and eject anyone who has left the Discord — this makes the Discord a live allow-list rather than a login-only check. The server calls an operator-authed endpoint:

POST /api/zones/servers/membership-check
Authorization: Bearer op_<key_id>.<secret>
Content-Type: application/json

{ "player_ids": ["<grid_user_id>", "..."] }

Response: the subset of IDs that are definitively no longer members (transient errors are omitted, so a Discord blip never kicks anyone):

{
  "blocked": ["<grid_user_id>"],
  "invite_url": "https://discord.gg/..."
}

The game client surfaces the same friendly "join our Discord" gate in-game, returns the player to the main menu, and offers a one-click retry after they rejoin.

Device Linking

The device-code flow lets the game client authenticate without a browser embedded in the game.

Step 1: Generate Code

From the game client:

POST /auth/device/request

Response:

{
  "code": "ABC123",
  "expires_in": 600,
  "interval": 5,
  "verification_url": "https://a-new-world.com/auth/link?code=ABC123"
}

Codes are 6-character alphanumeric and expire after 10 minutes. When a code expires, the game client requests a fresh one automatically and updates the QR code / link shown on the login screen — a browser tab opened with an old code will not work.

Step 2: User Links

The player visits the verification_url in their browser, logs in via Discord, and the page links the device code to their account:

POST /auth/device/verify
Content-Type: application/json

{ "code": "ABC123", "token": "<web_jwt>" }

Response (success):

{ "ok": true }

Errors:

StatusBodyMeaning
404{ "error": "code not found" }Code never existed (typo)
409{ "error": "code already used" }Code was already linked to an account
410{ "error": "code expired" }Code is older than 10 minutes — use the fresh code now shown in the game
401{ "error": "invalid token" }Web session JWT is invalid or expired

Step 3: Game Polls

The game client polls the API with the device code until the link is confirmed:

POST /auth/device/poll
Content-Type: application/json

{ "code": "ABC123" }

Response (success — code is consumed and deleted):

{
  "status": "complete",
  "token": "<jwt_token>"
}

Response (pending, HTTP 202):

{
  "status": "pending"
}

Response (expired, HTTP 410): { "error": "expired" } — the game requests a new code and continues polling.