API Reference
API Reference
All admin endpoints are mounted under /admin/system/ and require a valid admin session JWT (role === 'admin', tier Admin). Each endpoint additionally requires a specific permission resolved from the caller’s admin role in ADMIN_DB.
The full surface is published in the live OpenAPI spec at GET /api/openapi.json (and the Scalar UI at /docs) under the Admin System tag, and cataloged in the machine-readable manifest docs/admin/admin-manifest.json (regenerate with deno task admin:manifest). Both are derived from a single source (worker/routes/admin-system.openapi.ts), and CI fails if a mounted admin route is missing from either — so the panel, API, and docs stay in lockstep (epic · G).
Base URL: https://your-worker.workers.dev
Authentication: All requests require Authorization: Bearer <token>.
API keys: admin operations are also reachable with a blq_ API key, but access is opt-in — the key must carry the admin scope (a key owned by an admin without that scope is rejected with 403 "API key missing required scope: admin"). Keys are a permissioned, audited alternative to a session, not a bypass; destructive operations still additionally require Cloudflare Access.
Roles
List Roles
GET /admin/system/rolesPermission: admin:read
Response 200:
{ "success": true, "roles": [ { "id": 1, "role_name": "viewer", "display_name": "Viewer", "description": "Read-only access to admin dashboards and logs", "permissions": "[\"admin:read\",\"audit:read\",\"metrics:read\",\"config:read\",\"users:read\",\"flags:read\"]", "is_active": 1, "created_at": "2025-01-01T00:00:00", "updated_at": "2025-01-01T00:00:00" } ]}Create Role
POST /admin/system/rolesPermission: roles:write
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
role_name | string | ✅ | Unique role identifier (e.g. flag-manager) |
display_name | string | ✅ | Human-readable name |
description | string | Role description | |
permissions | string[] | ✅ | Array of permission strings (min 1) |
curl -X POST /admin/system/roles \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "role_name": "flag-manager", "display_name": "Flag Manager", "description": "Manages feature flags only", "permissions": ["admin:read", "flags:read", "flags:write"] }'Response 201:
{ "success": true, "role": { "id": 4, "role_name": "flag-manager", "..." : "..." }}Update Role
PATCH /admin/system/roles/:idPermission: roles:write
Path Parameters: id — Role ID (integer)
Request Body (all fields optional):
| Field | Type | Description |
|---|---|---|
display_name | string | Updated display name |
description | string | Updated description |
permissions | string[] | Replace permission array (min 1) |
is_active | boolean | Enable/disable the role |
Response 200:
{ "success": true, "role": { "id": 4, "role_name": "flag-manager", "..." : "..." }}List Role Assignments
GET /admin/system/roles/assignmentsPermission: admin:read
Query Parameters (all optional):
| Param | Type | Description |
|---|---|---|
clerk_user_id | string | Filter by user |
role_name | string | Filter by role |
curl "/admin/system/roles/assignments?role_name=editor" \ -H "Authorization: Bearer $JWT"Response 200:
{ "success": true, "assignments": [ { "id": 1, "clerk_user_id": "user_2abc123", "role_name": "editor", "assigned_by": "user_2xyz789", "assigned_at": "2025-01-15T10:30:00", "expires_at": null } ]}Assign Role
POST /admin/system/roles/assignPermission: roles:assign
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
clerk_user_id | string | ✅ | Clerk user ID to assign the role to |
role_name | string | ✅ | Role to assign (viewer, editor, super-admin, or custom) |
expires_at | string | ISO 8601 expiry date (null = never expires) |
curl -X POST /admin/system/roles/assign \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "clerk_user_id": "user_2abc123", "role_name": "editor", "expires_at": "2025-12-31T23:59:59Z" }'Response 200:
{ "success": true, "assignment": { "clerk_user_id": "user_2abc123", "role_name": "editor", "assigned_by": "user_2xyz789", "expires_at": "2025-12-31T23:59:59Z" }}Uses
INSERT ... ON CONFLICT ... DO UPDATE— re-assigning updates the existing assignment.
Revoke Role
DELETE /admin/system/roles/revokePermission: roles:assign
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
clerk_user_id | string | ✅ | User to revoke from |
role_name | string | ✅ | Role to revoke |
curl -X DELETE /admin/system/roles/revoke \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "clerk_user_id": "user_2abc123", "role_name": "editor" }'Response 200:
{ "success": true, "message": "Role revoked" }KV cache for the user is invalidated automatically.
My Context
Get My Context
GET /admin/system/my-contextPermission: None required (returns the caller’s own context)
Returns the full dashboard context for the authenticated admin, including role, permissions, tiers, scopes, flags, and active announcements.
curl /admin/system/my-context -H "Authorization: Bearer $JWT"Response 200:
{ "success": true, "context": { "clerk_user_id": "user_2abc123", "role_name": "super-admin", "permissions": ["admin:read", "admin:write", "..."], "expires_at": null }}Get My Permissions
GET /admin/system/my-permissionsPermission: None required
Returns only the caller’s resolved permissions array.
Tiers
List Tiers
GET /admin/system/tiersPermission: admin:read
Response 200:
{ "success": true, "tiers": [ { "id": 1, "tier_name": "anonymous", "order_rank": 0, "rate_limit": 10, "display_name": "Anonymous", "description": "Unauthenticated user — basic access", "features": "{\"maxSources\": 3, \"maxBatchSize\": 1}", "is_active": 1, "created_at": "2025-01-01T00:00:00", "updated_at": "2025-01-01T00:00:00" } ]}Update Tier
PUT /admin/system/tiers/:namePermission: config:write
Path Parameters: name — Tier name (e.g. free, pro)
Request Body (all fields optional):
| Field | Type | Description |
|---|---|---|
rate_limit | number | Requests per minute (0 = unlimited) |
display_name | string | Human-readable name |
description | string | Tier description |
features | object | JSON feature flags/capabilities |
is_active | boolean | Enable/disable the tier |
curl -X PUT /admin/system/tiers/pro \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "rate_limit": 500, "features": {"maxSources": 100, "maxBatchSize": 50, "priorityQueue": true} }'Response 200:
{ "success": true, "tier": { "tier_name": "pro", "..." : "..." } }Delete Tier
DELETE /admin/system/tiers/:namePermission: config:write
Path Parameters: name — Tier name
Response 200:
{ "success": true, "message": "Tier deleted" }Scopes
List Scopes
GET /admin/system/scopesPermission: admin:read
Response 200:
{ "success": true, "scopes": [ { "id": 1, "scope_name": "compile", "display_name": "Compile", "description": "Compile and download filter lists", "required_tier": "free", "is_active": 1 } ]}Update Scope
PUT /admin/system/scopes/:namePermission: config:write
Path Parameters: name — Scope name (e.g. compile, rules)
Request Body (all fields optional):
| Field | Type | Description |
|---|---|---|
display_name | string | Human-readable name |
description | string | Scope description |
required_tier | string | Minimum tier required (anonymous, free, pro, admin) |
is_active | boolean | Enable/disable the scope |
curl -X PUT /admin/system/scopes/compile \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "required_tier": "pro" }'Delete Scope
DELETE /admin/system/scopes/:namePermission: config:write
Endpoint Auth Overrides
Per-endpoint authentication requirements that override the global defaults.
List Endpoint Overrides
GET /admin/system/endpointsPermission: admin:read
Response 200:
{ "success": true, "endpoints": [ { "id": 1, "path_pattern": "/api/rules/*", "method": "*", "required_tier": "pro", "required_scopes": "[\"rules\"]", "is_public": 0, "is_active": 1 } ]}Create Endpoint Override
POST /admin/system/endpointsPermission: config:write
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
path_pattern | string | ✅ | URL pattern (e.g. /compile, /api/rules/*) |
method | string | HTTP method or * for all (default: *) | |
required_tier | string | Tier override (null = use default) | |
required_scopes | string[] | Scope overrides (null = use default) | |
is_public | boolean | true = no auth required |
curl -X POST /admin/system/endpoints \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "path_pattern": "/api/public/health", "method": "GET", "is_public": true }'Update Endpoint Override
PUT /admin/system/endpoints/:idPermission: config:write
Path Parameters: id — Override ID (integer)
Request Body (all fields optional):
| Field | Type | Description |
|---|---|---|
required_tier | string | Updated tier requirement |
required_scopes | string[] | Updated scope requirements |
is_public | boolean | Toggle public access |
is_active | boolean | Enable/disable the override |
Delete Endpoint Override
DELETE /admin/system/endpoints/:idPermission: config:write
Configuration Import / Export
The entire runtime-editable configuration — tiers, scopes, endpoint auth
overrides, announcements, admin roles, user groups, and API groups — can be
exported as a single bundle, edited offline, and re-imported. ADMIN_DB is the
source of truth; a bundle is a point-in-time snapshot that round-trips
(export → edit → import) to reproduce configuration. Bundles serialize to
JSON, YAML, or TOML (select with ?format=; TOML omits null-valued
fields since it has no null literal).
Export Config
GET /admin/system/config/export?format=jsonPermission: config:read
Streams the bundle as a file download (Content-Disposition: attachment).
format is one of json (default), yaml, toml.
curl "/admin/system/config/export?format=yaml" \ -H "Authorization: Bearer $JWT" -o bloqr-admin-config.yamlImport Config (dry-run + apply)
POST /admin/system/config/import?format=json[&dryRun=false]Permission: config:write
The request body is the serialized bundle (content matching format). By
default this is a dry run returning a per-section diff (added / removed /
changed / unchanged) against the live config — nothing is written:
{ "success": true, "dryRun": true, "diff": { "tiers": { "added": ["pro"], "removed": [], "changed": ["free"], "unchanged": 0 }, "...": {} } }Add ?dryRun=false to apply. Before writing, the current config is captured
as a snapshot (so the change can be reverted), then every section is upserted by
its natural key (create when absent, update when present). Volatile columns
(id, created_at, updated_at) are ignored. A malformed bundle is rejected
with 422 and no writes occur.
Defense-in-depth — applying a bundle (and reverting, below) rewrites the whole config, so when Cloudflare Access is configured these ops additionally require a valid
CF-Access-JWT-Assertionon top ofconfig:write(403otherwise). Dry-run previews are exempt. In deployments without CF Access this check no-ops.
{ "success": true, "dryRun": false, "snapshotId": 42, "applied": { "tiers": { "created": ["pro"], "updated": ["free"], "failed": [] }, "...": {} } }curl -X POST "/admin/system/config/import?format=json&dryRun=false" \ -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \ --data-binary @bloqr-admin-config.jsonThe natural keys are: tier_name (tiers), scope_name (scopes),
path_pattern + method (endpoints), id (announcements), role_name
(roles), group_name (groups and api-groups).
List Snapshots
GET /admin/system/config/snapshotsPermission: config:read
Returns snapshot metadata (id, created_at, created_by, reason), newest
first. Bundle bodies are omitted.
Get Snapshot
GET /admin/system/config/snapshots/:idPermission: config:read
Returns a single snapshot with its full bundle. 404 if not found.
Revert to Snapshot
POST /admin/system/config/snapshots/:id/revertPermission: config:write
Applies the snapshot’s bundle. The current config is itself snapshotted first
(reason: pre-revert:<id>) so the revert can be undone. Returns the apply
report plus preRevertSnapshotId.
curl -X POST "/admin/system/config/snapshots/42/revert" \ -H "Authorization: Bearer $JWT"Data-Subject Export (GDPR)
Export a User’s Admin Data
GET /admin/system/users/:userId/data-exportPermission: users:read
Aggregates everything the admin subsystem (ADMIN_DB) holds about a single
user — role assignments, group memberships, and the audit trail of actions they
performed — into one bundle, plus a data_locations manifest (store + table +
row count) answering “where does this user’s data reside?”. Read-only; the
export itself is audit-logged (user.data_export). Audit rows are paged in and
capped, with audit_log_truncated set when older entries are omitted.
This covers the admin-domain slice only; the user’s primary profile,
subscription, and API-usage records live in the application database and are
exported by that subsystem (see the notes field in the response).
{ "success": true, "export": { "user_id": "user_123", "exported_at": "2026-07-13T…", "data_locations": [ { "store": "ADMIN_DB", "table": "admin_role_assignments", "record_count": 1, "description": "…" }, { "store": "ADMIN_DB", "table": "admin_group_members", "record_count": 2, "description": "…" }, { "store": "ADMIN_DB", "table": "admin_audit_logs", "record_count": 37, "description": "…" } ], "role_assignments": [ … ], "group_memberships": [ … ], "audit_log_entries": [ … ], "audit_log_truncated": false, "notes": [ … ] }}curl "/admin/system/users/user_123/data-export" \ -H "Authorization: Bearer $JWT" -o user_123-admin-data.jsonErase a User’s Admin Data (delete-or-explain)
POST /admin/system/users/:userId/data-erasePermission: users:manage — plus Cloudflare Access (destructive
defense-in-depth) when CF Access is configured.
Removes the erasable admin-domain data for the user — role assignments and
group memberships — and explains what is deliberately retained. The audit
log (admin_audit_logs) is append-only and kept for security / accountability /
legal compliance, so it is reported under retained, not deleted. The erasure
is itself audit-logged (user.data_erase) and the user’s role-cache entry is
invalidated.
{ "success": true, "erasure": { "user_id": "user_123", "erased_at": "2026-07-13T…", "deleted": { "role_assignments": 1, "group_memberships": 2 }, "retained": [ { "store": "ADMIN_DB", "table": "admin_audit_logs", "record_count": 37, "reason": "Audit log is append-only and retained …" } ], "notes": [ … ] }}The user’s primary profile, subscription, and usage records live in the application database and are erased by that subsystem.
Feature Flags
Feature flags are managed via Cloudflare Flagship. These routes are a thin
proxy over the Flagship management API (worker/handlers/admin-flagship.ts) and return 503 if
CF_ACCOUNT_ID, FLAGSHIP_APP_ID, or FLAGSHIP_API_TOKEN aren’t configured. Flag evaluation
inside the Worker does not use these routes — see the [[flagship]] binding.
List Flags
GET /admin/config/feature-flagsPermission: admin:read
Response 200:
{ "success": true, "total": 1, "items": [ { "key": "new-parser-v2", "enabled": true, "default_variation": "off", "variations": { "off": false, "on": true }, "rules": [ { "conditions": [{ "attribute": "plan", "operator": "in", "value": ["pro", "admin"] }], "priority": 0, "serve_variation": "on", "rollout": { "percentage": 25 } } ], "description": "New AGTree v2 parser", "type": "boolean", "updated_at": "2026-06-24T10:30:00Z", "updated_by": "admin@bloqr.dev" } ]}Create Flag
POST /admin/config/feature-flagsPermission: flags:write
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
key | string | ✅ | Unique flag identifier (letters, numbers, hyphens, underscores) |
enabled | boolean | Default: false | |
default_variation | string | ✅ | Variation served when no rule matches or the flag is disabled |
variations | object | ✅ | Map of variation name → value; all values must share the same type |
rules | array | Targeting rules, evaluated in ascending priority. Default: [] | |
description | string | Human-readable description | |
type | string | boolean | string | number | json; inferred from variations if omitted |
curl -X POST https://api.bloqr.dev/admin/config/feature-flags \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "key": "streaming-api-beta", "enabled": true, "default_variation": "off", "variations": { "off": false, "on": true }, "rules": [{ "conditions": [], "priority": 0, "serve_variation": "on", "rollout": { "percentage": 10 } }], "description": "Server-sent events streaming API" }'Update Flag
PATCH /admin/config/feature-flags/:keyPermission: flags:write
Path Parameters: key — Flag key (string)
Request Body: Same fields as create (minus key), all optional — only the provided fields
change. Reads the current definition first and merges your payload on top of it, since Flagship’s
own update endpoint replaces the entire flag; this means a plain {"enabled": false} toggle never
wipes out existing rules or variations.
Delete Flag
DELETE /admin/config/feature-flags/:keyPermission: flags:write
Permanently deletes the flag. Cannot be undone.
Audit Logs
Query Audit Logs
GET /admin/system/auditPermission: audit:read
Query Parameters (all optional):
| Param | Type | Default | Description |
|---|---|---|---|
actor_id | string | Filter by Clerk user ID | |
action | string | Filter by action (e.g. tier.update) | |
resource_type | string | Filter by resource type (e.g. feature_flag) | |
resource_id | string | Filter by specific resource ID | |
status | string | success, failure, or denied | |
since | string | ISO 8601 start date | |
until | string | ISO 8601 end date | |
limit | number | 50 | Results per page (max 100) |
offset | number | 0 | Pagination offset |
curl "/admin/system/audit?action=tier.update&since=2025-01-01T00:00:00Z&limit=20" \ -H "Authorization: Bearer $JWT"Response 200:
{ "success": true, "logs": [ { "id": 42, "actor_id": "user_2abc123", "actor_email": "admin@example.com", "action": "tier.update", "resource_type": "tier_config", "resource_id": "pro", "old_values": "{\"rate_limit\": 300}", "new_values": "{\"rate_limit\": 500}", "ip_address": "203.0.113.1", "user_agent": "Mozilla/5.0...", "status": "success", "metadata": null, "created_at": "2025-01-15T10:30:00" } ], "total": 142, "limit": 20, "offset": 0}Announcements
List Announcements
GET /admin/system/announcementsPermission: admin:read
Response 200:
{ "success": true, "announcements": [ { "id": 1, "title": "Scheduled Maintenance", "body": "The platform will be briefly unavailable on Jan 20 from 02:00-04:00 UTC.", "severity": "warning", "active_from": "2025-01-18T00:00:00", "active_until": "2025-01-20T04:00:00", "is_active": 1, "created_by": "user_2abc123", "created_at": "2025-01-15T10:30:00", "updated_at": "2025-01-15T10:30:00" } ]}Create Announcement
POST /admin/system/announcementsPermission: config:write
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
title | string | ✅ | Announcement title |
body | string | Announcement body (supports markdown) | |
severity | string | info, warning, error, or success (default: info) | |
active_from | string | ISO 8601 start time (null = immediately) | |
active_until | string | ISO 8601 end time (null = no expiry) |
curl -X POST /admin/system/announcements \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{ "title": "New Parser Available", "body": "AGTree v2 is now available for all Pro users.", "severity": "success", "active_until": "2025-02-01T00:00:00Z" }'Update Announcement
PATCH /admin/system/announcements/:idPermission: config:write
Path Parameters: id — Announcement ID (integer)
Request Body: Same fields as create, all optional. Additionally:
| Field | Type | Description |
|---|---|---|
is_active | boolean | Enable/disable the announcement |
Delete Announcement
DELETE /admin/system/announcements/:idPermission: config:write
Local Auth Users
CRUD endpoints for managing local JWT auth users. These endpoints are available when the Worker is configured with a D1 DB binding and JWT_SECRET. They mirror Clerk’s Backend API for easy migration.
Authentication required: Admin tier + admin role (enforced by checkRoutePermission).
Note: Password hashes are never returned. The
api_disabledflag can be toggled to block a user’s API access without deleting the account.
List Local Auth Users
GET /admin/local-usersQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Max results per page (1–100) |
offset | integer | 0 | Pagination offset |
Response:
{ "success": true, "users": [ { "id": "uuid", "identifier": "user@example.com", "identifier_type": "email", "role": "user", "tier": "free", "api_disabled": 0, "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" } ], "total": 42, "limit": 50, "offset": 0}Get Local Auth User
GET /admin/local-users/:idPath Parameters: id — User UUID
Response: { "success": true, "user": { ... } } — same user object shape as the list items above.
Create Local Auth User
POST /admin/local-usersRequest Body:
| Field | Type | Required | Description |
|---|---|---|---|
identifier | string | ✅ | Email or phone number |
password | string | ✅ | Plaintext password (hashed server-side with PBKDF2) |
role | string | user or admin (default: user) | |
tier | string | Override tier; defaults to the role’s default tier |
Response: 201 with the created user object.
Errors:
409 Conflict— identifier already registered
Update Local Auth User
PATCH /admin/local-users/:idRequest Body (at least one field required):
| Field | Type | Description |
|---|---|---|
role | string | New role (user or admin) |
tier | string | New tier (free, pro, admin) |
api_disabled | 0 | 1 | 1 blocks all API access for this user |
Tier and role are independent. Setting
rolewithouttierauto-derives the default tier for that role. Setting both allows overrides (e.g.{ "role": "user", "tier": "pro" }for a Pro-rate-limited non-admin user).
Response: Updated user object.
Delete Local Auth User
DELETE /admin/local-users/:idResponse: { "success": true, "message": "User deleted" }
Errors:
404 Not Found— user does not exist
API Usage
Query per-user API usage statistics stored in KV. Useful for monitoring, abuse detection, and billing.
Authentication required: Admin tier + admin role.
Get User Usage
GET /admin/usage/:userIdPath Parameters: userId — User UUID (local auth user ID)
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
days | integer | 30 | Lookback window in days (1–90) |
Response:
{ "success": true, "userId": "uuid", "total": { "count": 1234, "firstSeen": "2025-01-01T00:00:00Z", "lastSeen": "2025-03-15T12:00:00Z" }, "days": [ { "date": "2025-03-15", "count": 42, "routes": { "/compile": 38, "/validate": 4 } } ], "lookbackDays": 30}Endpoint Summary
| # | Method | Path | Permission |
|---|---|---|---|
| 1 | GET | /admin/system/roles | admin:read |
| 2 | POST | /admin/system/roles | roles:write |
| 3 | PATCH | /admin/system/roles/:id | roles:write |
| 4 | GET | /admin/system/roles/assignments | admin:read |
| 5 | POST | /admin/system/roles/assign | roles:assign |
| 6 | DELETE | /admin/system/roles/revoke | roles:assign |
| 7 | GET | /admin/system/my-context | (none) |
| 8 | GET | /admin/system/my-permissions | (none) |
| 9 | GET | /admin/system/tiers | admin:read |
| 10 | PUT | /admin/system/tiers/:name | config:write |
| 11 | DELETE | /admin/system/tiers/:name | config:write |
| 12 | GET | /admin/system/scopes | admin:read |
| 13 | PUT | /admin/system/scopes/:name | config:write |
| 14 | DELETE | /admin/system/scopes/:name | config:write |
| 15 | GET | /admin/system/endpoints | admin:read |
| 16 | POST | /admin/system/endpoints | config:write |
| 17 | PUT | /admin/system/endpoints/:id | config:write |
| 18 | DELETE | /admin/system/endpoints/:id | config:write |
| 19 | GET | /admin/config/feature-flags ⁺ | admin:read |
| 20 | POST | /admin/config/feature-flags ⁺ | flags:write |
| 21 | PATCH | /admin/config/feature-flags/:key ⁺ | flags:write |
| 22 | DELETE | /admin/config/feature-flags/:key ⁺ | flags:write |
| 23 | GET | /admin/system/audit | audit:read |
| 24 | GET | /admin/system/announcements | admin:read |
| 25 | POST | /admin/system/announcements | config:write |
| 26 | PATCH | /admin/system/announcements/:id | config:write |
| 27 | DELETE | /admin/system/announcements/:id | config:write |
| 28 | GET | /admin/local-users | Admin role |
| 29 | GET | /admin/local-users/:id | Admin role |
| 30 | POST | /admin/local-users | Admin role |
| 31 | PATCH | /admin/local-users/:id | Admin role |
| 32 | DELETE | /admin/local-users/:id | Admin role |
| 33 | GET | /admin/usage/:userId | Admin role |
| 34 | GET | /admin/system/config/export | config:read |
| 35 | POST | /admin/system/config/import | config:write |
| 36 | GET | /admin/system/config/snapshots | config:read |
| 37 | GET | /admin/system/config/snapshots/:id | config:read |
| 38 | POST | /admin/system/config/snapshots/:id/revert | config:write |
| 39 | GET | /admin/system/users/:userId/data-export | users:read |
| 40 | POST | /admin/system/users/:userId/data-erase | users:manage + CF Access |
⁺ Live and wired into worker/routes/admin.routes.ts today. The /admin/system/* routes are mounted
via worker/routes/admin-system.routes.ts, which adapts the worker/handlers/admin-handlers.ts
handlers (roles, tiers, scopes, endpoint overrides, audit, announcements) and the native config
import/export handlers onto the Hono router behind the coarse /admin/* entry gate.
Note — user groups (
/admin/system/groups/*) and API groups (/admin/system/api-groups/*) are also mounted (epic · B) but not yet enumerated in this table; their OpenAPI documentation lands with epic · F.