API reference

Admin endpoints

Every /admin/* route, what it does, and the response shape.

/admin/* requires a session and a Steam ID on the ADMIN_STEAM_IDS env allowlist. Empty allowlist → every admin endpoint returns 503.

Ping

GET /admin/ping
→ { admin: true, steamId: "...", allowlistSize: 3 }

The admin portal uses this to gate the UI.

Members

GET /admin/users?q=<search>&limit=50&offset=0
→ { total: number, users: User[] }

GET /admin/users/:id
→ { user: User & { subscription, pairedServers, fcmCredential, dashboardGuestInvites } }

Online overview (combined)

GET /admin/users/overview?q=<search>&limit=200
→ {
    relayReachable: boolean,
    relayError?: string,
    totals: { totalUsers, online, paid, beta, withCreds, withDiscord, withPaired, stuck: {...} },
    users: StatusUser[]
  }

Combines DB read + live relay presence + beta-tester lookup + guest-host lookup. Auto-refresh-friendly (the admin's User Status page polls this every 15s). funnelStage values: no-subscription, no-credentials, no-paired-server, ready, guest-ready.

Paired servers

DELETE /admin/paired-servers/:id
DELETE /admin/users/:userId/paired-servers

Items / Conveyor map

GET    /admin/items
PUT    /admin/items/:id
DELETE /admin/items/:id
POST   /admin/items/upload-icon       (multipart, max 5MB)
GET    /admin/items-search?q=...

GET    /admin/conveyor-map
POST   /admin/conveyor-map            { itemId }
PUT    /admin/conveyor-map/:shortname
DELETE /admin/conveyor-map/:shortname
POST   /admin/conveyor-map/bulk-category

Stores (CRUD on JSON blobs)

GET /admin/store/:name        → { data: any }
PUT /admin/store/:name        body: { data: any }
GET /admin/stores             → { stores: [...] }

name is one of: messages, crafting, raid, faq. Each row is a JSON blob the dashboard / bot reads at runtime — no redeploy needed to edit.

Beta testers

GET    /admin/beta-testers
POST   /admin/beta-testers   body: { steamId, tier, note?, expiresAt? }
DELETE /admin/beta-testers/:steamId

Refunds (Stripe)

POST /admin/refund   body: { chargeId? | paymentIntentId?, amount?, reason? }
GET  /admin/refunds?limit=20

Analytics

GET /admin/analytics
→ {
    generatedAt,
    members: { total, dau, wau, mau },
    subscriptions: { active, trialing, pastDue },
    pairings7d,
    activityByType7d: [{ type, count }],
    eventsByType7d: [{ type, count }]
  }

Audit log

GET /admin/audit?limit=200&action=items.put
→ { total, entries: AuditEntry[] }

Every admin write is recorded with actor steam ID, action, target, payload, result, createdAt.

Discord registration

POST /admin/discord/register-commands?guildId=<optional>
→ { registered: 4, scope: 'global' | 'guild' }

Pushes the support-bot's slash-command schema to Discord. Run after every change in lib/discord/commands.ts.

Wiki management (new)

GET  /admin/wiki/tree                 → { sections: NavSection[] }
GET  /admin/wiki/page?slug=index      → { slug, frontmatter, body }
PUT  /admin/wiki/page                 body: { slug, frontmatter, body }
DELETE /admin/wiki/page?slug=...
POST /admin/wiki/image                multipart, → { url }

See the admin's Wiki tab for the UI.