admin
Warmbly's internal admin control plane. Separate Vite + React app, parallel to web/, that drives the /admin/* endpoints on the same backend.
Why a separate app
The dashboard at web/ is the product surface for customers. The admin app is the surface for the Warmbly team running the platform. Splitting them gives us:
- a smaller, faster admin bundle (no tiptap, no marketing chrome, no onboarding flow)
- independent deployment cadence (admin can ship without touching customer code)
- different origin in production, so a stolen dashboard session can't quietly use admin endpoints
- a clear visual marker (the amber ADMIN badge + stripe + sidebar tint) so anyone with both tabs open knows which one is which
Both apps share the same backend, the same Bearer-token auth shape, and the same shadcn primitives.
Run it locally
pnpm install
pnpm dev # boots on http://localhost:5174
pnpm build # production bundle into ./dist
pnpm typecheck # tsc -b
pnpm lint
From the repo root you can also use make admin, which is a shortcut for
cd admin && pnpm dev. make app does not start this app — admin lives
outside the docker compose stack so it can ship on its own cadence.
The dev server defaults to port 5174 so it coexists with the dashboard's 5173.
First admin (local dev)
Admin access is gated by users.admin_permissions (bitmask) on the backend.
Nothing in the codebase seeds the first admin — sign up through the dashboard
as normal, then promote yourself from the repo root:
make grant-admin EMAIL=you@example.com # super-admin
make grant-admin EMAIL=you@example.com ROLE=support # or ops, analyst
make revoke-admin EMAIL=you@example.com # drop back to 0
Role bitmasks mirror AdminRolePermissions in
internal/models/admin_permission.go. For one-off permission combinations,
pass a raw BITMASK=N instead of ROLE.
Once a super-admin exists they can grant the rest from Accounts > Admins,
which goes through the audited GrantAdminPermissions path instead of raw SQL.
Set up .env.local from .env.example:
cp .env.example .env.local
| Variable | Purpose |
|---|---|
VITE_API_URL |
Same Warmbly backend the dashboard talks to. Reuses /admin/*. |
VITE_ENV_LABEL |
Drives the Production / Staging / Development pill in the topbar. |
VITE_DASHBOARD_URL |
Used by the "Open dashboard" link in the user menu. |
Visual differentiation (do not strip)
This app is intentionally tinted differently from the dashboard. If you find yourself "cleaning up" the amber accent, stop and read this section first.
- ADMIN badge in the sidebar header and on the login card. Amber pill,
ShieldAlerticon. Always visible. - 3px stripe along the top of the app shell (
admin-stripeutility). First thing the eye lands on. - Sidebar tint (
--sidebarshifted warm + faint diagonal pattern viaadmin-sidebar-pattern) so the rail reads as a different surface than the dashboard's near-white sidebar. - Amber active-nav state instead of the dashboard's blue.
- Env pill in the topbar — different colour per environment.
- Title prefix:
index.htmlships<title>Admin · Warmbly</title>and the favicon is an amber-bordered shield (public/admin-icon.svg).
These signals are layered on purpose. A single one (e.g. just the badge) is easy to overlook in a tab strip. Stacked, they make it obvious that the user is in the privileged surface.
What is in it
Every nav entry is backed by real endpoints under /admin/*; there are no stub pages.
| Group | Pages |
|---|---|
| Overview | counters, trends, signups by channel, the instance problems strip |
| Operations | Workers, Fleet (capacity, decision log, dedicated bindings), Mailboxes, Sync (backfill and fair-use throttle per mailbox), Warmup (pools, abuse signals, action history), Warmup Appeals, Warmup Content, Campaigns, Sends (in-flight reservations, dead letters, task failures, webhook delivery health) |
| Accounts | Users, Organizations (with API keys, webhooks and transfer tabs), Limit requests, Outreach, Admins |
| Insight | Live Events (the admin:platform socket firehose), Audit Log, Jobs (every background loop with last run, next run and "run now") |
| Instance | Setup and health (findings and service probes), Configuration (settings, notifications, environment, effective limits), Transfers (workspace export and import) |
Cmd/Ctrl K opens a command palette that jumps to any page and searches users, organizations, mailboxes and workers. Lists stay live through the realtime invalidation spine in src/lib/realtime/RealtimeManager.tsx; only pages whose data has no event (service probes, jobs, in-flight sends, capacity) poll.
The customer docs describe the panel page by page at docs/content/docs/development/admin-panel.mdx.
Layout
admin/
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig*.json
├── eslint.config.js
├── components.json # shadcn config, mirrors web/
├── public/
│ └── admin-icon.svg # amber-stroked shield favicon
└── src/
├── main.tsx # router + query client + providers
├── global.css # design tokens (mirror of web/) + admin-only tokens
├── app/
│ ├── auth/LoginPage.tsx
│ └── dashboard/ # one file per page, tab bodies in subfolders
├── components/
│ ├── data/ # DataTable, Explorer facet rail
│ ├── layout/ # AppShell, Sidebar, MobileNav, Topbar, CommandPalette, PageTabs, …
│ └── ui/ # shadcn primitives copied from web/src/components/ui
├── hooks/
│ └── useMe.ts, useDocumentTitle.ts, useInstanceHealth.ts, …
└── lib/
├── env.ts
├── utils.ts
├── auth/storage.ts # Bearer token persistence
└── api/
├── client.ts # axios instance + Request<T>
├── client/
│ ├── auth/ # login, getMe, logout
│ └── admin/ # one module per backend area (workers, sync, sends, jobs, fleet, …)
└── models/