Authentication API
The API is split across two processes. Next.js (default :3000) serves thin proxy handlers under /api/*; the Express service (default :5002) owns all business logic, gating, and persistence. The browser never talks to Express directly.
Browser -> Next.js /api/* (pages/api/*.ts) -> Express /api/* (:5002)
The proxy (lib/apiProxy.ts → forwardToBackend()) forwards Authorization, X-Request-Id, and X-QA-Bypass-Key verbatim and rewrites the path 1:1. BACKEND_URL overrides the Express target. Server-to-server integrations may skip the proxy and call Express directly; the contracts below are identical either way. Every path in this reference is the shared /api/* path.
Request correlation
Send X-Request-Id to correlate a call across the proxy, Express access logs (morgan :req-id), and the structured logs. If omitted, the server assigns one and echoes it back on the response.
Credentials
Two credential types are accepted on protected routes, both via the Authorization header (JWT also via cookie in the browser session):
| Type | Presentation | Use |
|---|---|---|
| Session JWT | Authorization: Bearer eyJhbGciOiJIUzI1NiIs... | HS256, 8h expiry, issued by /api/auth/login. extractUser middleware decodes it and attaches req.user (id, email, role). |
| Personal API token | Authorization: Bearer sdbs_... or X-API-Key: sdbs_... | Non-interactive clients (CI, scripts). Created under Settings → API Tokens; the secret is shown once. Acts as the creating user (same role), optional expiry, revocable. Only the SHA-256 hash is stored server-side. |
Missing, invalid, expired, or revoked credentials return 401. In non-production only, X-QA-Bypass-Key and X-Agent-Key inject a virtual admin for test harnesses; both are hard-disabled when NODE_ENV=production.
License and role gating
Route families are gated by license tier (enforced by requireLicense / requireTier / requireAI) and, for some writes, by role (requireRole). Tier failures return 403; the license cache has a 60s TTL, so a freshly activated license may take up to a minute to take effect unless you re-activate or restart.
| Route family | Gate |
|---|---|
/api/health, /api/health/migrations, /api/license/*, /api/setup/* | Open (no license, no auth) |
/api/auth/*, /api/me, /api/roles, /api/onboarding | Licensed (any tier) + authenticated |
/api/connections, /api/databases, /api/migrations, /api/queries | Starter+ (authenticated); connection creation additionally passes enforceConnectionLimit |
/api/db-devops/* (scripts, releases, connections, executions, approvals, discovery, repo-connections, settings, compliance-rules, snapshots, consumers, shadow-runs, rbac) | Professional+ |
/api/db-devops/metrics, /api/notifications | Licensed (any tier) |
/api/db-devops/audit, /api/auth/idp | Enterprise |
/api/ai/* | AI-enabled license flag (requireAI) |
/api/data-protection/* | Licensed (any tier) |
Response envelopes
Two shapes appear across the API and both are in active use. Newer routes use the success/error envelope; several core routes return the resource (or an { error } object) directly. Treat any non-2xx as an error and read error.
{ "success": true, "data": ... }
{ "success": false, "error": "message" }
// or, on direct-return routes:
{ ...resource } // 2xx
{ "error": "message" } // non-2xx, sometimes with "code"
POST /api/auth/login
Exchange credentials for a JWT. No auth required.
curl -X POST http://localhost:5002/api/auth/login \\
-H "Content-Type: application/json" \\
-d '{ "email": "admin@company.com", "password": "SecureP@ssw0rd" }'
200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": { "id": 1, "email": "admin@company.com", "name": "Admin", "role": "admin" }
}
| Status | Body | Cause |
|---|---|---|
| 400 | {"error":"Email and password are required"} | Missing field |
| 401 | {"error":"Invalid credentials"} | Unknown email or wrong password (identical message prevents enumeration) |
| 429 | {"error":"Too many requests"} | General limiter tripped |
When MFA is enrolled the login flow requires the TOTP step (/api/auth/mfa/*) before a full-privilege token is issued.
POST /api/auth/register
Create a user. No auth required unless self-registration is disabled by policy. Returns 201 with the new id.
{ "email": "dev@company.com", "password": "SecureP@ssw0rd", "name": "Jane Developer" }
// 201 -> { "message": "User created successfully", "user": { "id": 2, "email": "dev@company.com" } }
Errors: 400 missing field; 409 email already exists.
Personal API tokens
| Method | Endpoint | Notes |
|---|---|---|
| GET | /api/auth/tokens | List your tokens (metadata only; never the secret) |
| POST | /api/auth/tokens | { "name": "...", "expiresInDays": 90 } → returns the plaintext sdbs_... once |
| DELETE | /api/auth/tokens/:id | Revoke |
Mounted before the generic /api/auth router, licensed + authenticated.
Session identity and roles
| Method | Endpoint | Returns |
|---|---|---|
| GET | /api/me | Current user profile derived from the credential |
| GET | /api/roles | Role catalog for RBAC-aware clients |
License lifecycle
| Method | Endpoint | Notes |
|---|---|---|
| POST | /api/license/activate | { "licenseKey": "SDB-{payload}.{sig}" }; verifies signature + expiry, refreshes the cache |
| GET | /api/license/status | Current tier, customer, expiry, maxConnections (-1 = unlimited) |
| DELETE | /api/license/activate | Deactivate; app returns to unlicensed state |
// GET /api/license/status (active)
{ "active": true, "tier": "professional", "customerName": "Acme Corp",
"expiresAt": "2026-01-15", "maxConnections": -1 }
// (none)
{ "active": false, "tier": null, "customerName": null, "expiresAt": null, "maxConnections": 0 }
Activation errors are all 400: missing key, malformed SDB-{payload}.{signature}, or signature/expiry validation failure.
Health
| Method | Endpoint | Notes |
|---|---|---|
| GET | /api/health | Overall status + changeops DB readiness. Returns STARTING until the DB is reachable (10× retry, 3s apart). |
| GET | /api/health/migrations | Applied internal migrations; used by deploy validators. |
Status codes
| Status | When |
|---|---|
| 200 / 201 / 202 | OK / created / accepted (async job queued, e.g. shadow run) |
| 400 | Missing or invalid parameters |
| 401 | No / invalid / expired / revoked credential |
| 403 | Valid credential but insufficient role or license tier |
| 404 | Resource does not exist |
| 409 | Conflict (duplicate, or FK reference blocking a delete) |
| 413 | Payload too large (e.g. SQL content over the 5MB per-script cap) |
| 429 | Rate limit exceeded |
| 500 | Unhandled server error (see logs by X-Request-Id) |
Rate limiting
Two per-IP limiters (express-rate-limit). Both configurable by env; defaults shown.
| Scope | Default | Window | Env |
|---|---|---|---|
General (/api/*) | 2000 | 15 min | GENERAL_RATE_LIMIT_MAX |
DB DevOps + auth (/api/db-devops/*, /api/auth/*) | 5000 | 1 min | DB_DEVOPS_RATE_LIMIT_MAX |
Standard X-RateLimit-Limit / -Remaining / -Reset headers are returned; a 429 carries Retry-After. The request body cap is 150mb.