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.tsforwardToBackend()) 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):

TypePresentationUse
Session JWTAuthorization: Bearer eyJhbGciOiJIUzI1NiIs...HS256, 8h expiry, issued by /api/auth/login. extractUser middleware decodes it and attaches req.user (id, email, role).
Personal API tokenAuthorization: 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 familyGate
/api/health, /api/health/migrations, /api/license/*, /api/setup/*Open (no license, no auth)
/api/auth/*, /api/me, /api/roles, /api/onboardingLicensed (any tier) + authenticated
/api/connections, /api/databases, /api/migrations, /api/queriesStarter+ (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/notificationsLicensed (any tier)
/api/db-devops/audit, /api/auth/idpEnterprise
/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" }
}
StatusBodyCause
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

MethodEndpointNotes
GET/api/auth/tokensList your tokens (metadata only; never the secret)
POST/api/auth/tokens{ "name": "...", "expiresInDays": 90 } → returns the plaintext sdbs_... once
DELETE/api/auth/tokens/:idRevoke

Mounted before the generic /api/auth router, licensed + authenticated.

Session identity and roles

MethodEndpointReturns
GET/api/meCurrent user profile derived from the credential
GET/api/rolesRole catalog for RBAC-aware clients

License lifecycle

MethodEndpointNotes
POST/api/license/activate{ "licenseKey": "SDB-{payload}.{sig}" }; verifies signature + expiry, refreshes the cache
GET/api/license/statusCurrent tier, customer, expiry, maxConnections (-1 = unlimited)
DELETE/api/license/activateDeactivate; 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

MethodEndpointNotes
GET/api/healthOverall status + changeops DB readiness. Returns STARTING until the DB is reachable (10× retry, 3s apart).
GET/api/health/migrationsApplied internal migrations; used by deploy validators.

Status codes

StatusWhen
200 / 201 / 202OK / created / accepted (async job queued, e.g. shadow run)
400Missing or invalid parameters
401No / invalid / expired / revoked credential
403Valid credential but insufficient role or license tier
404Resource does not exist
409Conflict (duplicate, or FK reference blocking a delete)
413Payload too large (e.g. SQL content over the 5MB per-script cap)
429Rate limit exceeded
500Unhandled server error (see logs by X-Request-Id)

Rate limiting

Two per-IP limiters (express-rate-limit). Both configurable by env; defaults shown.

ScopeDefaultWindowEnv
General (/api/*)200015 minGENERAL_RATE_LIMIT_MAX
DB DevOps + auth (/api/db-devops/*, /api/auth/*)50001 minDB_DEVOPS_RATE_LIMIT_MAX

Standard X-RateLimit-Limit / -Remaining / -Reset headers are returned; a 429 carries Retry-After. The request body cap is 150mb.