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.