Skip to content

Authentication Overview

VisibilityIQ has two distinct authentication systems, and which one applies depends on who is calling:

  • Session authentication — for browser traffic on the dashboard, sign-in/sign-up, and all /api/* routes that a logged-in user hits. This is handled by Better Auth v1.6.
  • API-key authentication — for machine-to-machine access to the public REST API under /api/v1/*. External integrations always use API keys.

The two never mix: the session middleware (src/middleware.ts) explicitly bypasses /api/v1/ because “the public REST API authenticates per-request with a Bearer API key (not a session cookie), so it bypasses the session middleware and self-gates.” Everything else under /api/ (except a short public allow-list) requires a valid session cookie.

The session layer is constructed per request by a factory, createAuth(env, cf, baseURL), in src/lib/auth/index.ts. Each Worker invocation builds a fresh Better Auth instance bound to that request’s bindings — the D1 database (env.DB), the KV namespace (env.SESSIONS, where sessions are persisted), and env.BETTER_AUTH_SECRET. Construction is synchronous and cheap, so no instance is cached across requests.

Email/password is the only first-party credential method, with a deliberately strict policy that should not be relaxed without an explicit decision:

SettingValue
requireEmailVerificationtrue — unverified users cannot sign in
minPasswordLength10
autoSignInfalse — sign-up does not auto-create a session
Password hashingcustom hashPassword / verifyPassword (src/lib/auth/password.ts)

Two side effects fire on sign-up. A databaseHooks.user.create.after hook auto-creates a personal organization for every new user (idempotent on the slug personal-<userId>), and a verification email is sent via Resend (sendOnSignUp: true). Email-send failures are logged, never thrown, so a degraded mailer never breaks the auth flow.

Sign-up and sign-in are additionally protected by a reCAPTCHA captcha plugin, but only when env.RECAPTCHA_SECRET_KEY is present. It gates exactly two endpoints — /sign-up/email and /sign-in/email. The browser must forward the solved token in the x-captcha-response header.

The legacy shared-password page (/login) issues a viq_session KV-backed cookie (7-day TTL) that the middleware checks first. Alongside it, the middleware dual-accepts a Better Auth session via auth.api.getSession({ headers }). During the /login/signin transition both are honoured; once cutover completes the legacy block is removed. See Admin / Operator Guard for how the /admin surface tightens this further.

The public REST API (/api/v1/*) is keyed, not session-cookied. Keys are minted in the dashboard; only the SHA-256 hash (api_keys.key_hash) and a display-only key_prefix are stored — the secret itself is shown once at creation and never persisted (migration 033_api_keys.sql). Each key carries its own scopes (JSON array), a credit_budget, and token-bucket rate state, so the gateway meters every request without extra storage.

A key’s stored key_prefix looks like viq_live_ab12. Include the full key, prefix and all, in the Authorization header:

Authorization: Bearer viq_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Every API request goes through this sequence:

  1. Token extraction — the API reads the Authorization header and extracts the bearer token
  2. Key lookup — the key is looked up in the database and validated (active, not expired, not revoked)
  3. Scope check — the key’s granted scopes are checked against the endpoint’s required scope
  4. Rate limit check — the request is checked against per-endpoint rate limits
  5. Request proceeds — if all checks pass, the handler executes

If any check fails, the API returns an appropriate error response before the handler runs.

HTTP statusMeaning
401 UnauthorizedMissing or invalid API key
403 ForbiddenValid key but insufficient scope for this endpoint
429 Too Many RequestsRate limit exceeded; check Retry-After header

All error responses include a JSON body:

{
"error": "Insufficient scope: requires backlinks:read",
"status": 403
}

API keys are created in the dashboard under Account → API Keys. See API Keys for the full walkthrough.

See Scopes for the complete scope listing and which endpoints require which scopes.

See Rate Limits for per-endpoint limits and how to handle 429 responses.