Admin App — Behavior
Authentication
- Clerk-managed. The SPA initializes
ClerkProviderwithVITE_CLERK_PUBLISHABLE_KEYand falls back to a hash-routed<SignIn />when signed out. - Refine
authProviderwrapsuseAuth();getToken()is threaded into every API call via the custom dataProvider. - Authorization: every
/admin/**API call passes throughPlatformAdminGuard(apps/api/src/admin/guards/platform-admin.guard.ts):- Read
clerkIdfromrequest.auth.userId. - Cache lookup
platform-admin:<clerkId>in Redis (5 min TTL). - On cache miss, fetch the Clerk user and check
privateMetadata.platformRole === 'admin'. - Cache the result (
trueorfalse) for 5 min. - Throw
ForbiddenExceptionotherwise.
- Read
A user demoted in Clerk continues to have access for up to 5 minutes — acceptable for a tool that’s used by trusted internal staff only. The cache key is invalidated manually via the admin user detail page when a demotion needs to take effect immediately.
Audit logging
Every state-changing call to /admin/** flows through AdminAuditInterceptor. The interceptor:
- Captures actor
clerkId, method, URL, body. - Writes to
audit_logswithactorClerkId,action,resource,resourceId,metadata.{ adminAction: true, ip, userAgent, ... }. - Pipes the entry through
EventTrackingServiceto PostHog so admin actions show on the same funnel as agent actions.
Reads are not audit-logged.
Pages
Organizations
GET /admin/organizations — list with pagination, filters (tier, status, country, search).
GET /admin/organizations/:id — full detail including members, subscriptions, plans, minisite status, recent audit entries.
Tier updates: POST /admin/organizations/:id/tier body { tier }, audited.
Users
GET /admin/users — cross-org user search.
GET /admin/users/:id — profile, memberships, payment cards.
Actions: password reset (POST /admin/users/:id/reset-password), soft-delete, force email update.
Payments + subscriptions
- Payment list filterable by provider, status, org, date range.
- Refund flow requires
reasonin the body — recorded into the audit metadata. - Subscription dashboard surfaces
next_charge_dateand overdue cohorts.
Leads
Cross-org lead pipeline. Owners reassign leads to orgs (e.g. a waitlist lead asking for a specific gym).
Jobs
Lists imports, exports, embedding, AI enrichment jobs across all orgs. Pause / requeue / drop helpers.
Audit Logs
Filterable by actor, resource, timestamp, metadata.agent, metadata.adminAction. The view is the canonical “what happened” timeline.
Queues
BullMQ status (waiting, active, completed, failed, delayed counts). Actions: pause, resume, drain. Powered by AdminQueuesService.
System
Server uptime, memory, Redis ping, Postgres + R2 health, env summary (no secrets).
Costs
Daily and monthly spend across providers (Anthropic, Voyage, Resend, etc.). Per-org breakdown surfaces cost outliers.
Billing inspection
Per-org invoice and balance state. Surfaces debt, past-due, cancellation requests.
Platform Billing
FitKit’s own income side. Lists platform transactions (apps/api/src/admin/controllers/admin-platform-billing.controller.ts), MRR, tier distribution, cancelled platform subscriptions. Includes a cancel-platform-txn.dto.ts action for refunding FitKit’s invoices.
Observability
Agent trace pivot, recent Sentry issues (linked out), PostHog quick links. Lightweight — heavy analysis lives in the underlying tools.
Actions
One-off operational hooks under apps/api/src/admin/controllers/admin-actions.controller.ts:
- Rebuild cache (
rebuild-cache.dto.ts). - Reindex embeddings (
reindex.dto.ts). - Fix bookings (
fix-bookings.dto.ts). - Manual cost entry (
manual-cost.dto.ts). - Each is audited.
Impersonate
POST /admin/impersonate/:userId returns a short-lived Clerk impersonation token. Admin loads the dashboard as the target user; the audit log carries impersonatedUserId. Used sparingly for ticket resolution.
Canonical Movements
Page for curating the canonical exercise library (used by the embedding + Spotter resolve flows). Editors review enrichment output, edit muscle/equipment tags, dedupe duplicates.
Deploy
- Vercel project, separate from
apps/web. - Build:
pnpm exec nx build admin-app→vite build→ SPA assets underdist/. - Hosted at a private subdomain; access is gated at app level (Clerk) — no IP allowlist.
Limits
- No mobile layout — Ant Design table density assumes desktop.
- Real-time updates (queues, jobs) poll every 10s; no SSE / WebSocket.
- Bulk operations are limited to what the API exposes; no client-side select-all + apply pattern beyond what Refine ships.