Authentication
Configure Atlas authentication — from no auth in development to enterprise SSO in production.
Atlas supports four authentication modes. Auth is optional — Atlas works with no auth for local development.
| Mode | Best for | How it works |
|---|---|---|
| None | Local dev, internal tools | No authentication. All requests are anonymous |
| Simple Key | Headless integrations, CLI, SDKs | Single shared secret via Authorization: Bearer <key> |
| Managed | Multi-user web apps | Email/password with sessions. Built on Better Auth |
| BYOT | Enterprise SSO (Auth0, Clerk, Okta) | Stateless JWT verification against your identity provider's JWKS endpoint |
Auto-detection
When ATLAS_AUTH_MODE is not set, Atlas auto-detects the mode from environment variables:
| Priority | Variable present | Mode selected |
|---|---|---|
| 1 | ATLAS_AUTH_JWKS_URL | BYOT |
| 2 | BETTER_AUTH_SECRET | Managed |
| 3 | ATLAS_API_KEY | Simple Key |
| 4 | None of the above | None |
Set ATLAS_AUTH_MODE explicitly to override auto-detection: none, api-key (alias: simple-key), managed, or byot.
No Auth
The default for local development. All requests pass through without authentication.
# No auth variables needed — just start the server
bun run devNo user identity is available. Rate limiting still works (keyed by IP). Audit logs record queries without user attribution.
Do not use no-auth mode in production unless Atlas is behind a VPN or reverse proxy that handles authentication externally.
Simple Key
A single shared secret for API access. Best for headless integrations, CLI tools, and automation.
Setup
# .env
ATLAS_API_KEY=your-secret-key-here
ATLAS_API_KEY_ROLE=analyst # Optional: viewer, analyst (default), adminClient usage
Send the key in the Authorization header (preferred) or X-API-Key header:
# Authorization header
curl -H "Authorization: Bearer your-secret-key-here" \
http://localhost:3001/api/v1/query \
-d '{"question": "How many users?"}'
# X-API-Key header (alternative)
curl -H "X-API-Key: your-secret-key-here" \
http://localhost:3001/api/v1/query \
-d '{"question": "How many users?"}'Security details
- Keys are compared using constant-time SHA-256 hash comparison (timing-safe)
- User IDs in logs are derived from a hash of the key — the raw key is never logged
- A single key maps to a single user identity and role
RLS with Simple Key
When using Row-Level Security with simple-key auth, provide static claims:
ATLAS_RLS_CLAIMS='{"tenant_id": "acme-corp"}'Managed Auth
Full user management with email/password login, sessions, and role-based access. Built on Better Auth. Requires an internal Postgres database (DATABASE_URL).
Setup
# .env
BETTER_AUTH_SECRET=your-random-secret-at-least-32-characters-long
DATABASE_URL=postgresql://user:pass@host:5432/atlas
# Optional
BETTER_AUTH_URL=https://api.example.com # Auto-detected on Vercel
BETTER_AUTH_TRUSTED_ORIGINS=https://app.example.com,https://admin.example.com
ATLAS_ADMIN_EMAIL=admin@example.com # First admin account
ATLAS_CORS_ORIGIN=https://app.example.com # Required for cross-origin cookiesBootstrap admin
On first deployment:
- If
ATLAS_ADMIN_EMAILis set, the user matching that email getsadminrole on signup - If
ATLAS_ADMIN_EMAILis not set, the first user to sign up getsadminautomatically - Subsequent signups get the
analystrole (set by Better Auth'sdefaultRolein the admin plugin)
Dev admin seeding
When ATLAS_ADMIN_EMAIL is set and no users exist in the database, Atlas automatically creates a dev admin account on startup:
- Email: the value of
ATLAS_ADMIN_EMAIL - Password:
atlas-dev - Role:
admin
The account is flagged with password_change_required -- the user must change this password after first login. This is intended for initial setup only. If any users already exist, seeding is skipped (idempotent).
How sessions work
- User signs in via
POST /api/auth/sign-in/email - Better Auth creates a session and sets a
better-auth.session_tokencookie - Subsequent requests include the cookie automatically (browser) or send
Authorization: Bearer <session-token>(API clients) - Sessions last 7 days. On any request where the session is at least 24 hours old, the expiry is automatically renewed
Users can log out via POST /api/auth/sign-out (clears the session cookie). Sessions are also revoked when the session expires (7 days) or if the user is deleted by an admin.
Cookie cache: Better Auth is configured with a 5-minute cookie cache (
cookieCache.maxAge = 300s). This means role changes and session revocations may take up to 5 minutes to take effect due to the cache. The cache improves performance by avoiding a database lookup on every request, but be aware of this delay when revoking access or changing user roles.
Cross-origin deployment
When the API and frontend are on different origins (e.g. api.example.com and app.example.com):
ATLAS_CORS_ORIGIN=https://app.example.com
BETTER_AUTH_TRUSTED_ORIGINS=https://app.example.com
BETTER_AUTH_URL=https://api.example.comATLAS_CORS_ORIGIN controls the HTTP Access-Control-Allow-Origin header (which origins can make API requests). BETTER_AUTH_TRUSTED_ORIGINS controls CSRF protection (which origins can submit auth forms). In most cross-origin setups, set both to the same value.
Both variables are required together. If you set
ATLAS_CORS_ORIGINbut notBETTER_AUTH_TRUSTED_ORIGINS, CORS will pass but CSRF protection will reject auth requests. If you setBETTER_AUTH_TRUSTED_ORIGINSbut notATLAS_CORS_ORIGIN, the browser will block cross-origin requests entirely. Always set both for cross-origin managed auth.
Cross-subdomain cookies: When
ATLAS_CORS_ORIGINandBETTER_AUTH_URLare on different subdomains (e.g.app.example.comandapi.example.com), the session cookie domain is automatically derived to the parent domain (.example.com). This allows cookies set by the API to be sent from the frontend subdomain. No manual cookie configuration is needed.
Frontend integration
The Atlas web UI includes a built-in auth client. For custom frontends, use the Better Auth React client:
import { createAuthClient } from "better-auth/react";
const auth = createAuthClient({
baseURL: "https://api.example.com",
});
// Sign up
await auth.signUp.email({
email: "user@example.com",
password: "secure-password",
name: "User Name",
});
// Sign in
await auth.signIn.email({
email: "user@example.com",
password: "secure-password",
});BYOT (Bring Your Own Token)
Stateless JWT verification against an external identity provider. Best for enterprises with existing SSO (Auth0, Clerk, Okta, Supabase Auth, etc.).
Setup
# .env
ATLAS_AUTH_JWKS_URL=https://your-idp.com/.well-known/jwks.json
ATLAS_AUTH_ISSUER=https://your-idp.com/ # Required
ATLAS_AUTH_AUDIENCE=your-atlas-api # Optional but recommended
ATLAS_AUTH_ROLE_CLAIM=app_metadata.role # Optional, defaults to "role" then "atlas_role"
ATLAS_AUTH_ISSUERis required for BYOT mode. Atlas will return a startup error ifATLAS_AUTH_JWKS_URLis set withoutATLAS_AUTH_ISSUER.
If ATLAS_AUTH_AUDIENCE is set, audience validation is enforced — JWTs without a matching aud claim are rejected. If unset, audience validation is skipped entirely.
How it works
- Client obtains a JWT from your identity provider (Auth0, Clerk, etc.)
- Client sends
Authorization: Bearer <jwt>to Atlas - Atlas verifies the JWT signature against the JWKS endpoint
- Atlas validates
iss(issuer) andaud(audience) claims - Atlas extracts user ID from
subclaim and role from the configured claim path
Provider examples
Auth0:
ATLAS_AUTH_JWKS_URL=https://your-tenant.auth0.com/.well-known/jwks.json
ATLAS_AUTH_ISSUER=https://your-tenant.auth0.com/
ATLAS_AUTH_AUDIENCE=https://api.example.com
ATLAS_AUTH_ROLE_CLAIM=https://example.com/rolesClerk:
ATLAS_AUTH_JWKS_URL=https://your-app.clerk.accounts.dev/.well-known/jwks.json
ATLAS_AUTH_ISSUER=https://your-app.clerk.accounts.dev
ATLAS_AUTH_ROLE_CLAIM=metadata.roleSupabase:
ATLAS_AUTH_JWKS_URL=https://your-project.supabase.co/auth/v1/.well-known/jwks.json
ATLAS_AUTH_ISSUER=https://your-project.supabase.co/auth/v1
ATLAS_AUTH_ROLE_CLAIM=app_metadata.roleRole claim extraction
The ATLAS_AUTH_ROLE_CLAIM supports dot-delimited paths for nested JWT structures:
# Top-level claim
ATLAS_AUTH_ROLE_CLAIM=role
# Nested claim
ATLAS_AUTH_ROLE_CLAIM=app_metadata.role
# Deeply nested
ATLAS_AUTH_ROLE_CLAIM=https://example.com/claims.atlas.roleIf not set, Atlas checks role then atlas_role at the top level. Valid roles: viewer, analyst, admin.
RLS with BYOT
The full JWT payload is available as user claims for Row-Level Security. This enables tenant isolation based on JWT claims:
ATLAS_RLS_ENABLED=true
ATLAS_RLS_COLUMN=tenant_id
ATLAS_RLS_CLAIM=org_id # Extracted from JWT payload
ATLAS_AUTH_ROLE_CLAIMvsATLAS_RLS_CLAIM: These serve different purposes.ATLAS_AUTH_ROLE_CLAIMextracts the user's role (viewer/analyst/admin) for permission checks.ATLAS_RLS_CLAIMextracts a value (e.g. a tenant ID) that gets injected as a WHERE clause filter on every query for row-level security.
Roles
Atlas has three roles with increasing privileges:
| Role | Level | Can query | Can approve actions | Can approve admin actions |
|---|---|---|---|---|
viewer | 0 | Yes | No | No |
analyst | 1 | Yes | Yes | No |
admin | 2 | Yes | Yes | Yes |
Default roles by auth mode:
- Simple Key:
analyst(override withATLAS_API_KEY_ROLE) - Managed:
analystfor new signups (first signup orATLAS_ADMIN_EMAILmatch getsadmin) - BYOT: Extracted from JWT claim (defaults to
viewerif missing)
Rate Limiting
Rate limiting is per-user, sliding-window (60-second window).
ATLAS_RATE_LIMIT_RPM=60 # 60 requests per minute per user
ATLAS_TRUST_PROXY=true # Required behind reverse proxy to get real IPsWhen rate limited, the API returns HTTP 429 with a Retry-After header.
Proxy deployments: Without
ATLAS_TRUST_PROXY=true, theX-Forwarded-ForandX-Real-IPheaders are ignored. Behind a reverse proxy (Railway, Vercel, nginx, etc.), all requests will appear to come from the proxy's IP, causing all users to share a single rate-limit bucket. Always setATLAS_TRUST_PROXY=truewhen deploying behind a proxy.
Rate limits apply to API requests, not agent steps. One chat request can trigger up to 25 internal agent steps.
Rate limit state is held in-memory. It resets when the process restarts and is not shared across horizontally scaled instances. For distributed rate limiting, use an external solution (e.g. a reverse proxy or API gateway).
Audit Logging
Every SQL query is logged with user attribution, regardless of auth mode.
Pino logs (always active): Structured JSON logs with user ID, query, duration, and success/failure. SQL truncated to 500 chars.
Internal database (when DATABASE_URL is set): Persistent audit_log table with full query text (truncated to 2000 chars), user identity, timing, and error details. Auto-migrated on startup.
Sensitive patterns (passwords, connection strings, credentials) are automatically scrubbed from both log destinations before writing.
Set ATLAS_LOG_LEVEL=debug to see per-step agent actions in the logs.