Auth Bootstrap Runbook
Auth Bootstrap Runbook
Step-by-step guide to bootstrap the admin user, run a smoke test, and create a Postman API key from scratch in a fresh production environment. All steps use shell variables so values captured in one step flow into the next automatically.
Prerequisites
curlandjqinstalledpsqlCLI with access to the Neon production database
Set these once before starting:
export API_BASE="https://api.bloqr.dev/api"export NEON_CONN="postgresql://neondb_owner:<password>@<host>.neon.tech/neondb?sslmode=require"export ADMIN_EMAIL="you@example.com"export ADMIN_PASSWORD="a-very-strong-password"Replace
<password>,<host>, and the database name with the values from your Neon project’s connection details page.
Step 1 — Sign Up
Create the initial user account. Better Auth creates it with tier: free and role: user
by default — promotion happens in Step 2.
SIGNUP_RESPONSE=$(curl -s -X POST "$API_BASE/auth/sign-up/email" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"Admin User\", \"email\": \"$ADMIN_EMAIL\", \"password\": \"$ADMIN_PASSWORD\" }")
echo "$SIGNUP_RESPONSE" | jq .
export USER_ID=$(echo "$SIGNUP_RESPONSE" | jq -r '.user.id')echo "User ID: $USER_ID"Expected response (200 OK):
{ "user": { "id": "01965f3a-...", "name": "Admin User", "email": "you@example.com", "tier": "free", "role": "user" }, "session": { "id": "01965f3b-...", "token": "sess_...", "expiresAt": "2026-05-06T00:00:00.000Z" }}Step 2 — Promote to Admin in Neon
Better Auth cannot self-promote the first admin — there is no existing admin to authorise
the set-role call. Promote directly via SQL using the neondb_owner role.
psql "$NEON_CONN" <<SQLUPDATE usersSET role = 'admin', tier = 'admin'WHERE id = '$USER_ID';SQLVerify the update:
psql "$NEON_CONN" -c "SELECT id, email, role, tier FROM users WHERE id = '$USER_ID';"Expected output:
id | email | role | tier--------------------------------------+------------------+-------+------- 01965f3a-... | you@example.com | admin | admin(1 row)Step 3 — Sign In and Capture the Bearer Token
Sign in to obtain a fresh session token. The token is returned in the response body
and is also set as the bloqr.session_token cookie.
SIGNIN_RESPONSE=$(curl -s -X POST "$API_BASE/auth/sign-in/email" \ -H "Content-Type: application/json" \ -d "{ \"email\": \"$ADMIN_EMAIL\", \"password\": \"$ADMIN_PASSWORD\" }")
echo "$SIGNIN_RESPONSE" | jq .
export BEARER_TOKEN=$(echo "$SIGNIN_RESPONSE" | jq -r '.session.token')echo "Bearer token: $BEARER_TOKEN"Expected response (200 OK):
{ "user": { "id": "01965f3a-...", "email": "you@example.com", "tier": "admin", "role": "admin" }, "session": { "id": "01965f3b-...", "token": "sess_...", "expiresAt": "2026-05-06T00:00:00.000Z" }}Verify that tier and role are both admin — confirming the promotion from Step 2.
Step 4 — Smoke Test the Auth Endpoints
Sign-out (requires an explicit empty JSON body)
POST /api/auth/sign-out must include Content-Type: application/json and
-d '{}'. Omitting the body causes a request error; the worker currently responds with
400 Bad Request for an invalid JSON body.
curl -s -X POST "$API_BASE/auth/sign-out" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $BEARER_TOKEN" \ -d '{}' | jq .Expected response (200 OK):
{ "success": true }Re-sign in to get a fresh token for the next steps
SIGNIN_RESPONSE=$(curl -s -X POST "$API_BASE/auth/sign-in/email" \ -H "Content-Type: application/json" \ -d "{ \"email\": \"$ADMIN_EMAIL\", \"password\": \"$ADMIN_PASSWORD\" }")
export BEARER_TOKEN=$(echo "$SIGNIN_RESPONSE" | jq -r '.session.token')echo "Fresh bearer token: $BEARER_TOKEN"List your API keys
curl -s "$API_BASE/keys" \ -H "Authorization: Bearer $BEARER_TOKEN" | jq .Expected response (200 OK):
{ "success": true, "keys": [], "total": 0}Step 5 — Create a Postman API Key
Use POST /api/keys with the Better Auth Bearer token. No X-Admin-Key is needed for
self-service key creation.
Note:
scopesdefaults to["compile"]if omitted. This example includes"scopes": ["compile"]explicitly for clarity.
KEY_RESPONSE=$(curl -s -X POST "$API_BASE/keys" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $BEARER_TOKEN" \ -d '{ "name": "Postman Testing", "scopes": ["compile"], "expiresInDays": 90 }')
echo "$KEY_RESPONSE" | jq .
export API_KEY=$(echo "$KEY_RESPONSE" | jq -r '.key')echo "API key: $API_KEY"Expected response (201 Created):
{ "success": true, "id": "...", "key": "abc_Xk9mP2...", "keyPrefix": "abc_Xk9m", "name": "Postman Testing", "scopes": ["compile"], "rateLimitPerMinute": 60, "expiresAt": "2026-07-29T00:00:00.000Z", "createdAt": "2026-04-29T00:00:00.000Z"}Copy the
keyvalue immediately — it is only returned once and cannot be retrieved again.
Step 6 — Postman Setup
Use the variables captured above to configure Postman for ongoing testing.
Create the Postman environment
- Open Postman → Environments → +
- Name it
bloqr-prod - Add the following variables:
| Variable | Type | Value |
|---|---|---|
baseUrl | default | https://api.bloqr.dev |
apiBase | default | https://api.bloqr.dev/api |
apiKey | secret | (paste the key value from Step 5) |
bearerToken | secret | (paste $BEARER_TOKEN from Step 3/4) |
- Click Save and select
bloqr-prodas the active environment.
Collection-level authorisation
- Create a new collection named
bloqr-prod - Open the collection → Authorization tab
- Set Type to
Bearer Tokenand Token to{{apiKey}} - Click Save
All requests in the collection will inherit {{apiKey}} automatically. Override
per-request as needed (e.g., to use {{bearerToken}} for key-management calls).
Quick verification request
GET https://api.bloqr.dev/api/versionNo auth required. Expected response:
{ "version": "0.x.x", "environment": "production" }Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
POST /api/auth/sign-out → 400 | Missing Content-Type or empty body | Add -H "Content-Type: application/json" -d '{}' |
Sign-in response shows tier: free / role: user after promotion | Neon promotion query didn’t commit | Re-run the UPDATE in Step 2; verify with the SELECT below it |
POST /api/keys → 403 Forbidden | Request used a non-interactive auth method (for example, API key-on-API-key) or failed the interactive-session guard | Sign in normally to obtain a fresh interactive Bearer token, then retry POST /api/keys with that token instead of an API key or other non-interactive credential |
POST /api/keys → key with empty scopes | scopes field omitted from request body | Pass "scopes": ["compile"] explicitly — the default is ["compile"] but explicit is safer |
401 Unauthorized in Postman | Expired Bearer token | Re-run Step 3/4 and update {{bearerToken}} in the environment |
Related Documentation
- Better Auth Admin Guide — User management, banning, role promotion via the API
- Better Auth User Guide — Full sign-up / sign-in / session reference
- API Authentication — API key scopes, limits, and usage
- Postman Testing — Postman collection setup and request examples
- Cloudflare Access — Protecting
/admin/*with Cloudflare Zero Trust