Skip to content

Operator Guide

Operator Guide

Step-by-step instructions for deploying, configuring, and maintaining the admin system.

Prerequisites

RequirementDetails
Clerk accountclerk.com — provides identity and JWT authentication
Cloudflare accountWorkers Paid plan (required for D1, KV, Analytics Engine)
Wrangler CLInpm i -g wrangler (v3+) — used for D1 management and deployments
Git accessClone of the bloqr-backend repository

1. Create the ADMIN_DB Database

The admin system uses a dedicated D1 database separate from the application database.

Terminal window
# Create the database
wrangler d1 create bloqr-backend-admin-db

This outputs a database_id UUID. Copy it — you’ll need it in the next step.

Update wrangler.toml

The ADMIN_DB binding is pre-configured but commented out in wrangler.toml. Uncomment it and insert your database ID:

[[d1_databases]]
binding = "ADMIN_DB"
database_name = "bloqr-backend-admin-db"
database_id = "<paste-your-database-id-here>"
migrations_dir = "admin-migrations"

Why a separate database? The admin system uses its own D1 instance (ADMIN_DB) as an edge cache, with Neon PostgreSQL (via Hyperdrive) as the authoritative data store. D1 provides low-latency reads at the edge while Neon holds the durable source of truth. The ADMIN_DB binding is separate from the application’s DB binding for blast-radius isolation — a bad admin migration or runaway query cannot corrupt application data.

2. Run Migrations

The schema is defined in admin-migrations/0001_admin_schema.sql and creates 8 tables with seed data.

Local Development

Terminal window
wrangler d1 migrations apply bloqr-backend-admin-db --local

This creates a local SQLite file that simulates D1 for development.

Production

Terminal window
wrangler d1 migrations apply bloqr-backend-admin-db --remote

Verify

Check that the tables were created and seeded:

Terminal window
# Local
wrangler d1 execute bloqr-backend-admin-db --local \
--command "SELECT role_name, display_name FROM admin_roles;"
# Remote
wrangler d1 execute bloqr-backend-admin-db --remote \
--command "SELECT role_name, display_name FROM admin_roles;"

Expected output:

role_namedisplay_name
viewerViewer
editorEditor
super-adminSuper Admin

3. Assign the First Super-Admin

This is a bootstrap problem: you need admin access to assign admin roles, but no one has admin access yet.

Step 1: Set Clerk Metadata

In the Clerk Dashboard (dashboard.clerk.com):

  1. Go to Users → find your user
  2. Click EditPublic metadata
  3. Set:
{
"tier": "admin",
"role": "admin"
}

This is the gate check — the middleware verifies publicMetadata.role === 'admin' before any permission evaluation.

Alternatively, use the Clerk Backend API:

Terminal window
# Find your user ID
curl https://api.clerk.com/v1/users?email_address=you@example.com \
-H "Authorization: Bearer $CLERK_SECRET_KEY"
# Set admin metadata
curl -X PATCH https://api.clerk.com/v1/users/{your_user_id}/metadata \
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"public_metadata": {"tier": "admin", "role": "admin"}}'

Step 2: Assign the Super-Admin Role in ADMIN_DB

Since there are no existing admin role assignments at bootstrap, the first super-admin must be seeded out-of-band via a direct D1 insert (the API endpoint requires roles:assign permission, which nobody holds yet):

Terminal window
# Using Wrangler to execute a D1 query directly
wrangler d1 execute bloqr-backend-admin-db --command \
"INSERT INTO admin_role_assignments (clerk_user_id, role_name, assigned_by)
VALUES ('user_2yourClerkId', 'super-admin', 'bootstrap')"

Or, if you prefer the Wrangler D1 interactive shell:

Terminal window
wrangler d1 execute bloqr-backend-admin-db --interactive
INSERT INTO admin_role_assignments (clerk_user_id, role_name, assigned_by)
VALUES ('user_2yourClerkId', 'super-admin', 'bootstrap');

Once you have confirmed the insert, subsequent role assignments can be made through the API (POST /admin/system/roles/assign) using your super-admin JWT.

Step 3: Verify

Terminal window
curl https://your-worker.workers.dev/admin/system/my-context \
-H "Authorization: Bearer $YOUR_CLERK_JWT"

You should see:

{
"success": true,
"context": {
"clerk_user_id": "user_2yourClerkId",
"role_name": "super-admin",
"permissions": ["admin:read", "admin:write", "...all 27..."],
"expires_at": null
}
}

4. KV Cache Management

Role resolution results are cached in KV for performance. The admin system shares the RATE_LIMIT KV namespace with the application rate limiter, using an admin: prefix.

Cache Configuration

SettingValue
NamespaceRATE_LIMIT (existing KV binding in wrangler.toml)
Key prefixadmin:role:{clerkUserId}
TTL300 seconds (5 minutes)
InvalidationAutomatic on role assign/revoke

Manual Cache Inspection

Terminal window
# List admin cache keys
wrangler kv key list --namespace-id=$RATE_LIMIT_NS_ID --prefix="admin:role:"
# Read a specific cache entry
wrangler kv get --namespace-id=$RATE_LIMIT_NS_ID "admin:role:user_2abc123"
# Manually delete a cache entry (force D1 re-lookup)
wrangler kv delete --namespace-id=$RATE_LIMIT_NS_ID "admin:role:user_2abc123"

Cache Staleness

If you modify a role’s permissions directly in D1 (e.g., via wrangler d1 execute), the KV cache will serve stale data for up to 5 minutes. To force immediate effect:

  1. Delete the affected user’s cache key (see above), or
  2. Wait 5 minutes for the TTL to expire.

The admin API handles cache invalidation automatically — this only applies to out-of-band D1 changes.

5. Environment Variables & Bindings

The admin system requires these bindings in wrangler.toml:

Required

BindingTypePurpose
ADMIN_DBD1 DatabaseAdmin configuration and audit storage

Shared with Application

BindingTypePurpose
RATE_LIMITKV NamespaceRole cache (admin: prefix)
ANALYTICSAnalytics EngineAdmin event reporting
CLERK_PUBLISHABLE_KEYSecretJWT verification
CLERK_SECRET_KEYSecretBackend API calls

wrangler.toml snippet

# Admin D1 database (uncomment after creating)
[[d1_databases]]
binding = "ADMIN_DB"
database_name = "bloqr-backend-admin-db"
database_id = "<your-database-id>"
migrations_dir = "admin-migrations"
# Shared KV (already configured for rate limiting)
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "<your-kv-namespace-id>"
# Analytics Engine (already configured)
[[analytics_engine_datasets]]
binding = "ANALYTICS"
dataset = "bloqr_compiler_analytics"

6. Ongoing Operations

Adding New Admins

As a super-admin, you can onboard new administrators:

  1. Have the user create a Clerk account (sign up via the web UI).
  2. Set their Clerk publicMetadata.role to "admin" (via Clerk Dashboard or API).
  3. Assign them an appropriate admin role:
Terminal window
# Read-only access
curl -X POST /admin/system/roles/assign \
-H "Authorization: Bearer $JWT" \
-d '{"clerk_user_id": "user_2newAdmin", "role_name": "viewer"}'
# Config management access
curl -X POST /admin/system/roles/assign \
-H "Authorization: Bearer $JWT" \
-d '{"clerk_user_id": "user_2newAdmin", "role_name": "editor"}'
# Full admin access (use sparingly)
curl -X POST /admin/system/roles/assign \
-H "Authorization: Bearer $JWT" \
-d '{"clerk_user_id": "user_2newAdmin", "role_name": "super-admin"}'

Rotating or Revoking Access

Terminal window
# Revoke a role
curl -X DELETE /admin/system/roles/revoke \
-H "Authorization: Bearer $JWT" \
-d '{"clerk_user_id": "user_2former", "role_name": "editor"}'
# Also remove Clerk admin metadata to fully revoke access
curl -X PATCH https://api.clerk.com/v1/users/{user_id}/metadata \
-H "Authorization: Bearer $CLERK_SECRET_KEY" \
-d '{"public_metadata": {"role": null}}'

Monitoring Audit Logs

Regularly review the audit log for unexpected changes:

Terminal window
# Check for denied access attempts
curl "/admin/system/audit?status=denied&limit=50" \
-H "Authorization: Bearer $JWT"
# Review all config changes in the last 24 hours
curl "/admin/system/audit?since=$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)&limit=100" \
-H "Authorization: Bearer $JWT"

Database Maintenance

D1 is managed by Cloudflare, but you can run maintenance commands:

Terminal window
# Check table sizes
wrangler d1 execute bloqr-backend-admin-db --remote \
--command "SELECT name, (SELECT COUNT(*) FROM admin_audit_logs) as audit_count FROM sqlite_master WHERE type='table' LIMIT 1;"
# Backup (export)
wrangler d1 export bloqr-backend-admin-db --remote --output admin-backup.sql

Troubleshooting

”ADMIN_DB is not defined”

The ADMIN_DB binding is commented out in wrangler.toml. Follow step 1 above to create and configure it.

”403 Forbidden — not an admin”

The user’s Clerk publicMetadata.role is not set to "admin". Update it in the Clerk Dashboard.

”403 Forbidden — insufficient permission”

The user has an admin role but it lacks the required permission. Check their role:

Terminal window
curl "/admin/system/roles/assignments?clerk_user_id=user_2abc123" \
-H "Authorization: Bearer $JWT"

Then check the role’s permissions:

Terminal window
curl /admin/system/roles -H "Authorization: Bearer $JWT"

Role changes not taking effect

KV cache may be serving stale data. Either wait 5 minutes or manually invalidate:

Terminal window
wrangler kv delete --namespace-id=$RATE_LIMIT_NS_ID "admin:role:user_2abc123"