Admin Dashboard
The admin dashboard is a web UI that wraps the full Admin API. It is served from the same Worker as the API, but on the dedicated admin host:
https://admin.bve.meThe public API remains on https://api.bve.me. The dashboard is operator-facing and is not accessible to regular API key holders.
All mutations (key creation, suspension, revocation, model blocking) still flow through the Admin API.
-
Navigate to
https://admin.bve.me/login. -
Enter your operator email and password. Enable Remember session to prefill the email field on future visits.
-
On success, the server issues a
bve_admin_sessioncookie. A fresh CSRF token is also returned and stored in the browser’s module scope. -
The dashboard redirects you to the overview page at
/.
Session details
Section titled “Session details”| Property | Value |
|---|---|
| Cookie name | bve_admin_session |
| Flags | HttpOnly, Secure, SameSite=Lax, Path=/ |
| TTL | 7 days (sliding — extended every 15 minutes on activity) |
| Token storage | SHA-256 hash stored in D1; raw token only in the cookie |
| Logout | Soft-revokes the session and records an admin.logout audit event |
Sessions are visible on the Settings → Operator Profile page. You can revoke individual sessions or sign out all other devices from there.
Rate limiting
Section titled “Rate limiting”The login endpoint enforces a limit of 5 failures per email address or per IP address within a 15-minute window. A 429 response includes a Retry-After header.
Password management
Section titled “Password management”Operators can change their password from Settings → Operator Profile. Changing the password does not invalidate existing sessions.
To reset a forgotten password, an owner must run the CLI command:
bun run admin:reset-password --remoteThree roles control what operators can do.
| Capability | Owner | Admin | Viewer |
|---|---|---|---|
| Create / rotate / revoke API keys | ✓ | ✓ | — |
| Suspend / unsuspend API keys | ✓ | ✓ | — |
| Block / enable models (allowlist) | ✓ | ✓ | — |
| Purge in-memory caches | ✓ | ✓ | — |
| Send webhook test events | ✓ | ✓ | — |
| View request logs, usage, audit logs | ✓ | ✓ | ✓ |
| View analytics charts | ✓ | ✓ | ✓ |
| Create / delete operators | ✓ | — | — |
| Change other operators’ roles | ✓ | — | — |
Viewers have read-only access and see no mutating action buttons.
Navigation
Section titled “Navigation”The sidebar is divided into four sections.
Core Dashboard
- Overview — KPI cards and traffic chart
- Usage Analytics — analytics charts and model breakdowns
Access Control
- API Keys — key management and quota inspection
- Operators — operator accounts and roles
System Metrics (visible but unrelated endpoints may be hidden for viewers)
- Request Logs — sampled request log explorer
- Model Catalog — live Fuelix catalog and global policy controls
- Diagnostics — public API health, drift checks, and local audit runbooks
Configuration
- Audit Journal — operator and key lifecycle events
- Settings & Security — profile, team, gateway config, danger zone
Keyboard shortcuts
Section titled “Keyboard shortcuts”| Shortcut | Action |
|---|---|
Cmd K / Ctrl K | Open command palette |
Esc | Close any open panel or dialog |
The command palette provides quick navigation to every page and a Purge Cache action.
Sidebar extras
Section titled “Sidebar extras”- Search — type to filter sidebar links instantly
- Pinned views — hover any link and click the star to pin it at the top of the sidebar; pins persist to
localStorage - History — the sidebar shows your last three visited pages below the nav groups
Overview page
Section titled “Overview page”The overview (/) gives a live health summary.
KPI cards
Section titled “KPI cards”| Card | Details |
|---|---|
| Active API Keys | Count of active keys; expired keys shown separately as an alert |
| Monthly Requests | Progress bar against the hard cap; shows month-over-month delta |
| Monthly Tokens | Total token volume vs. prior month |
| D1 Database | Health check for the D1 database |
24-hour traffic chart
Section titled “24-hour traffic chart”Stacked bar chart showing successful requests (teal) and errors (rose) per hour over the last 24 hours. Shows total requests, error rate, and peak hour. Refreshes every 60 seconds; a manual refresh button is available.
Operational alerts
Section titled “Operational alerts”The dashboard derives alerts from live data. No configuration is required. Alerts fire for:
- DB health failure
- Suspended keys
- Expired keys (status
activebut pastexpires_at) - Keys expiring within 7 days
- Monthly hard cap reached (503 enforcing)
- Soft cap exceeded or at ≥ 80%
- Monthly request quota warning (key at 80–99% of
monthly_limit) - Monthly request quota exceeded (key at ≥ 100% of
monthly_limit) - Monthly token quota warning (key at 80–99% of
monthly_token_limit) - Monthly token quota exceeded (key at ≥ 100% of
monthly_token_limit)
Notification bell
Section titled “Notification bell”The bell icon in the top header shows an unread count (capped at 9+). Notifications cover:
| Notification type | Trigger |
|---|---|
key_expiring_soon | Key expires within the next 7 days |
key_expired | Key has passed its expires_at (status still active) |
key_quota_warning | Key has used 80–99% of its monthly request limit |
key_quota_exceeded | Key has used ≥ 100% of its monthly request limit |
key_token_quota_warning | Key has used 80–99% of its monthly token limit |
key_token_quota_exceeded | Key has used ≥ 100% of its monthly token limit |
Notifications are generated by the daily cron handler (0 3 * * *) and delivered via the EVENTS_QUEUE. They appear in the bell on the next dashboard load after the cron fires.
Clicking any notification deep-links to the API Keys page with that key’s Quota Inspector pre-opened.
API Keys page
Section titled “API Keys page”The API Keys page (/api-keys) is the primary key management interface.
List, search, and filter
Section titled “List, search, and filter”- Status tabs — All / Active / Suspended / Revoked / Expired, each with a live count badge.
- Debounced search — filters by key name (server-side, 500 ms debounce).
- CSV export — exports the current filtered set as a CSV file.
- Pagination — 50 rows per page.
Key table columns
Section titled “Key table columns”| Column | Details |
|---|---|
| Key Name & ID | Friendly name and UUID |
| Status | Animated badge (active / suspended / revoked / expired) |
| Last Used | Relative timestamp |
| Limits | RPM / RPD caps |
| Monthly Caps | Mini progress bars for requests and tokens; amber at 80%, rose at 95% |
| Allowed Models | Badge list, or a gateway-policy label explaining that the key follows the gateway’s live/global model policy when no per-key allowlist is set |
| Created | Creation date |
| Expires | UTC-aware status badge and detail line; exact expiry timestamps stay exact, while UTC end-of-day expiries render as a UTC date |
Bulk actions
Section titled “Bulk actions”Check one or more non-revoked keys to reveal a floating toolbar:
- Suspend N — calls
POST /admin/api-keys/bulk-suspend - Unsuspend N — calls
POST /admin/api-keys/bulk-unsuspend - Revoke N — shows a confirmation dialog, then calls
POST /admin/api-keys/bulk-revoke
Select-all picks every non-revoked key on the current page.
Per-key actions
Section titled “Per-key actions”Right-click the actions dropdown on any row (owner / admin only):
| Action | Description |
|---|---|
| Inspect Live Quota | Opens the real-time Quota Inspector modal |
| View Request Logs | Navigates to /transactions?keyId=<id> |
| Duplicate Key | Clones the config and generates a new raw key |
| Adjust Configuration | Opens the edit dialog |
| Suspend / Unsuspend | Toggles based on current status |
| Reset Usage Window | Opens a dialog to reset minute / day / month / all quota counters |
| Rotate Secret | Replaces the key material; the new raw key is shown once |
| Revoke Permanently | Confirmation dialog, then irrevocable |
Creating a key
Section titled “Creating a key”Click Create API Key and fill in:
- Name — descriptive label (required)
- RPM limit — requests per minute cap (
null= no limit) - RPD limit — requests per day cap (
null= no limit) - Monthly request cap — monthly request ceiling (
null= no limit) - Monthly token cap — monthly token ceiling (
null= no limit) - Allowed models — multi-select from the global catalog (
null= allow all) - Expiry date — ISO 8601 date, clearable (
null= never expires)
On success, the raw sk-bve-… key is displayed exactly once in a reveal dialog with a copy button. It cannot be recovered later — only rotated.
Quota Inspector modal
Section titled “Quota Inspector modal”Open via Inspect Live Quota or by visiting /api-keys?keyId=<uuid>. The modal polls every 5 seconds.
It displays:
- RPM gauge (used / limit, remaining slots, next reset)
- RPD gauge
- Monthly request cap gauge (if set)
- Monthly token cap gauge (if set)
- 7-day request history bar chart
- 24-hour activity stacked bar chart
- Performance Analytics with date tabs (7d / 30d / 90d / All): request count, error rate, 4xx / 5xx counts, avg / p50 / p95 / max latency, total tokens, top models bar chart
Request Logs page
Section titled “Request Logs page”The Request Logs page (/transactions) is a sampled request log explorer. The log is a 1% sample of all gateway requests — it is not a full audit trail.
KPI tiles
Section titled “KPI tiles”- Total Logged — request count in the filtered window
- Avg Latency — with p50 / p95 sub-text
- Token Volume — total prompt + completion tokens
- Error Rate — 4xx / 5xx breakdown; highlighted red if > 5%
Filters (left sidebar)
Section titled “Filters (left sidebar)”| Filter | Options |
|---|---|
| Status | All / 2xx / 4xx / 5xx |
| Date Range | From / to date pickers |
| Model | Free-text, Apply button |
| Key | Dropdown grouped as Active/Suspended vs. Revoked |
| Request ID | UUID input, Go button (exact lookup) |
| Error Code | Quick-select buttons for common codes + free-text |
| Latency Range | Min / max ms |
| Upstream Latency Range | Min / max ms upstream (Fuelix) time |
Deep-links are supported: /transactions?model=gpt-4o, /transactions?keyId=<uuid>, /transactions?requestId=<uuid>.
Request Log Inspector panel
Section titled “Request Log Inspector panel”Click any row to open a slide-in detail panel:
- Endpoint path, HTTP status, and error code
- Latency, tokens, model, and API key name
- Upstream vs. gateway overhead progress bar
- Token breakdown (prompt / completion / total)
- Request ID with a copy button
- cURL Replay — auto-generated curl command for the endpoint and model, with a copy button
Press Esc or click the close button to dismiss.
CSV export
Section titled “CSV export”Exports all rows matching the current filters, not just the current page.
Usage / Analytics page
Section titled “Usage / Analytics page”The Usage page (/usage) shows aggregate usage trends.
Controls
Section titled “Controls”- Key filter — aggregate all keys or isolate one
- Compare to prior period checkbox — overlays a dashed line for the previous period with a delta % badge
- Date range tabs — 7d / 30d / 90d
Charts and tables
Section titled “Charts and tables”| Section | Description |
|---|---|
| Daily Snapshots | Line chart of requests + tokens per day |
| Monthly Aggregates | Grouped bar chart |
| Top Models by Usage | Sortable: requests, tokens, latency, error rate. Clicking a row navigates to /transactions?model=<id> |
| Response Status Distribution | Horizontal bars: 2xx success / 4xx client errors / 5xx server errors |
| Requests by Endpoint | Bar chart and table per /v1/* path |
| Top Keys by Usage | Sortable leaderboard. Clicking navigates to /transactions?keyId=<id> |
| Recent Sampled Requests | Last N requests with endpoint, model, status, latency, tokens |
Models page
Section titled “Models page”The Models page (/models) compares the live Fuelix catalog for the current account with the gateway’s persistent D1 policy overrides.
Dashboard data contract
Section titled “Dashboard data contract”The page is backed by the session-authenticated dashboard endpoint GET /api/models, which returns:
{ "catalogAvailable": true, "models": [], "policyModels": []}| Field | Meaning |
|---|---|
catalogAvailable | true when the live Fuelix /models request succeeded; false when the page is operating in degraded mode |
models | Live Fuelix catalog rows for the current account |
policyModels | Preserved D1 allowlist rows that are not represented in models; when catalogAvailable=false, all preserved allowlist rows move here for review |
The row-level available flag means effective BVE routability after D1 global block policy is applied, not simply “Fuelix listed this model”. Live Fuelix-listed unclassified rows now remain routable by default, while listed_unavailable, registry_broken, and non-live rows remain blocked.
The dashboard no longer emits a separate snapshotModels array. Local registry rows that are absent from the live Fuelix catalog only surface here when a preserved D1 policy row still exists for the same model, in which case they appear in policyModels with a stale policy availability such as allowlist_only.
The page also performs a separate admin-only public drift probe through GET /api/public-api-catalog-probe when an optional server-side BVE_API_KEY binding is configured for the dashboard Worker. That probe compares the public https://api.bve.me/v1/models response seen by the configured user key with the dashboard’s current live Fuelix-backed catalog, normalised to that same key’s allowed_models restrictions so per-key filtering is not mistaken for deploy drift.
Availability states
Section titled “Availability states”availability | Meaning |
|---|---|
callable | Fuelix lists the model and the gateway treats it as callable by default on the current account |
listed_unavailable | Fuelix lists the model, but live account checks still fail; the gateway blocks it even if a D1 allow entry exists |
registry_broken | The model is intentionally classified as broken in the local registry |
unclassified | Fuelix lists the model, but the local registry has not yet classified it |
allowlist_only | A preserved D1 policy row that does not appear in the latest live Fuelix catalog |
allowlist_unverified | Fuelix is currently unavailable, so the preserved D1 policy row cannot yet be confirmed as live or stale |
Summary cards and operator queues
Section titled “Summary cards and operator queues”Above the main table, the page shows six summary cards:
- Fuelix Catalog — live catalog row count, or
—when Fuelix is unavailable - Verified Callable — models with
availability: "callable"in the current local snapshot - Listed / Failing — live catalog rows that still fail account-level checks
- Policy Rows — preserved D1 rows that are either stale (
allowlist_only) or temporarily unverified (allowlist_unverified) - Global Blocks — models with an explicit D1 block (
globallyEnabled: false) - Active — sampled request volume for the selected stats window
When the dashboard build and the public api.bve.me deploy drift apart, the page shows inline warnings that call out:
- the stale public deploy tag
- any
/healthheader mismatch (for example missingCross-Origin-Opener-Policy: same-origin) - public
/v1/modelscatalog drift, including missing live rows and stale deployments that still omitbve_available/bve_availability - representative public
/v1/models/:iddetail-route drift, including stale callable-model detail annotations and blocked-model routes that still return an upstream-style404instead of the gateway’s403 model_not_available
If the optional dashboard BVE_API_KEY binding is absent, the page degrades to health-only public warnings and explicitly says that the public catalog probe is not configured.
The Operator Queue groups the most actionable rows:
- Live Probe Failures — Fuelix-listed models with captured direct account-probe failures such as authorization problems, upstream-missing rows, or adapter breakage
- Traffic On Unavailable — sampled traffic landing on models that are not callable by default
- Registry Exceptions — live Fuelix rows that are still unclassified locally or intentionally marked broken
- Policy Cleanup Queue or Policy Review Queue — preserved policy rows that need cleanup, or temporary review while Fuelix is down
- Live Global Blocks — live catalog models that are blocked gateway-wide and still have sampled traffic
Category filter tabs
Section titled “Category filter tabs”All / Chat / Reasoning / Embedding / Image / TTS / Transcription / OCR / Not Default Callable, each with a count badge.
The page also supports free-text search plus provider and availability filters. The Policy Rows availability filter intentionally matches both allowlist_only and allowlist_unverified.
There is no standalone snapshot-excluded filter in this surface because non-live local registry rows without preserved policy state are not emitted by GET /api/models.
Live Fuelix catalog table
Section titled “Live Fuelix catalog table”| Column | Details |
|---|---|
| Model ID | Monospace, selectable |
| Category | chat / embedding / image / etc. |
| Capabilities | Badge list |
| Endpoints | /v1/chat/completions, /v1/embeddings, etc. |
| Origin Provider | Derived from model ID prefix |
| Requests | Sortable |
| Avg Latency | Sortable |
| p50 Latency | Sortable median latency |
| p95 Latency | Sortable tail latency |
| Err% | Sortable |
| Health / Policy | Health badge (Callable, Listed / Failing, Broken, Unclassified, or policy-row maintenance states) plus policy badge (Policy Open / Global Block) |
| Action | Block / Enable button (owner / admin only) |
Only live catalog rows from models appear in this table. Preserved D1-only rows from policyModels are rendered in a separate review section below so stale policy entries never pollute the current Fuelix catalog view.
The dashboard session API writes to /api/model-allowlist, which uses the same payload shape as POST /admin/model-allowlist. New writes must reference exact live Fuelix model IDs; stale policy-only rows are cleanup-only and are removed via DELETE instead of being re-posted:
- Block sends
{"model": "<id>", "enabled": false} - Enable sends
{"model": "<id>", "enabled": true}
Changes take effect within 30 seconds as the in-memory allowlist cache expires.
Policy cleanup / review queue
Section titled “Policy cleanup / review queue”Rows from policyModels render in a separate section below the live table:
- When
catalogAvailable=true, the section is labeled Policy Cleanup Queue and rows useavailability: "allowlist_only". These are stale D1 entries that no longer map to any live Fuelix row. - When
catalogAvailable=false, the section is labeled Policy Review Queue and rows useavailability: "allowlist_unverified". These rows stay visible so operators can review prior overrides, but they cannot yet be treated as stale cleanup entries.
Each policy row can be removed entirely. The underlying Admin API mutation is DELETE /admin/model-allowlist/:model.
When Fuelix is unavailable, the main live catalog table is empty until the upstream catalog recovers; only the preserved policy review queue remains visible.
If a stale policy row originally came from a local snapshot_excluded model, that context is still preserved in the row’s default availability metadata even though the dashboard no longer renders a separate snapshot-only collection. The policy queue and inspector surface that preserved default-availability context so operators can tell whether the stale row last looked callable, broken, snapshot-excluded, listed-but-failing, or unknown.
Model Detail Inspector panel
Section titled “Model Detail Inspector panel”Click any row to open a detail panel with:
- Category, provider label, full model ID, effective routing state, and policy state
- Capabilities and supported BVE endpoints
- Usage stats (requests, tokens, latency percentiles, error rate, token breakdown)
- Endpoint distribution with relative progress bars
- Block / Enable toggle for live catalog rows (owner / admin only)
- Remove policy entry action for
policyModelsrows - “View Request Logs for this model” deep-link
Audit Logs page
Section titled “Audit Logs page”The Audit Logs page (/audit-logs) shows operator and key lifecycle events recorded by the gateway.
Category filter tabs
Section titled “Category filter tabs”All Logs / Sessions (auth) / API Keys / Models / Operators
Columns
Section titled “Columns”| Column | Details |
|---|---|
| Timestamp | UTC, relative label |
| Action | Color-coded badge: create=emerald, suspend=amber, revoke=rose, login=indigo, logout=purple |
| Actor Type | admin or system |
| Target Resource | UUID or model ID (monospace) |
| Actor | Email + IP address (from metadata) |
| Raw Entry | JSON metadata preview, truncated to 120 chars |
Export as CSV respects the active filters.
Operators page
Section titled “Operators page”The Operators page (/users) is visible to all roles but mutating actions are owner-only.
Per-operator actions (owner only)
Section titled “Per-operator actions (owner only)”- Change role (viewer → admin → owner)
- Disable / activate the account
- Delete the account permanently
Create Operator
Section titled “Create Operator”Opens a dialog with fields: name, email, password (min 8 chars, toggleable visibility), and role (viewer / admin / owner).
Settings page
Section titled “Settings page”Operator Profile tab
Section titled “Operator Profile tab”- Current operator info (name, email, role, status)
- Session cookie details
- Change Password form (does not invalidate existing sessions)
- Active Sessions table — shows device, IP, sign-in time, last active, and expiry for each session. Per-session Revoke and bulk “Sign out other devices” buttons.
Team & Access tab
Section titled “Team & Access tab”- Live operator accounts table from D1
- Role Permissions Matrix — full table of all capabilities vs. owner / admin / viewer
Gateway Config tab
Section titled “Gateway Config tab”- API base URL and an OpenAI SDK snippet pre-populated with the configured URL
- Webhook integration status (
WEBHOOK_URLandWEBHOOK_SECRET) and a Send Test Event button - Global Rate Limits (soft cap and hard cap from
wrangler.jsoncvars)
Danger Zone tab
Section titled “Danger Zone tab”- Purge Gateway Caches — type
PURGE CACHESexactly to enable the button. Clears in-memory caches (model allowlist cache, monthly token totals cache). Keys, quotas, and logs are unaffected.
Security architecture
Section titled “Security architecture”| Mechanism | Implementation |
|---|---|
| Transport | HTTPS only (api.bve.me is TLS-terminated at Cloudflare) |
| CSRF protection | x-csrf-token header required on all mutating API endpoints; compared with constantTimeEqual |
| Session storage | SHA-256 hashed tokens in D1; raw token only in HttpOnly cookie |
| Password hashing | PBKDF2 with a per-password salt; iteration count within Workers Web Crypto limits |
| Timing attack defence | Sentinel hash always computed even when email is not found |
| Login rate limit | 5 failures per email/IP within 15 minutes |
| IP extraction | CF-Connecting-IP preferred over X-Forwarded-For |
| Audit trail | Every login, logout, key change, and model change is recorded in the audit log |
See Security Notes for the full security architecture of the gateway.