Security model
This is the access-control side of Data handling. Read both before answering procurement security questions.
Authentication
Section titled “Authentication”Two ways to authenticate, picked per use case.
Session-based (web app)
Section titled “Session-based (web app)”Email/password and OAuth (Google, GitHub) accounts produce a JWT
access token + refresh token pair on login. The web app stores them
and sends the access token as Authorization: Bearer <jwt> on every
protected request.
- Access token lifetime: 30 minutes by default
(
JWT_ACCESS_TOKEN_EXPIRE_MINUTES). - Refresh token lifetime: 7 days by default
(
JWT_REFRESH_TOKEN_EXPIRE_DAYS). - Algorithm: HS256 (
JWT_ALGORITHM), signed withJWT_SECRET_KEY. The settings module refuses to start in production if these secrets are still at their default placeholder values. - Revocation: every JWT carries a session ID (
sid). Logging out, revoking via Active Sessions, or any session being deleted invalidates the token even if it hasn’t expired — the auth middleware checks the session record on every request.
API-key (CLI, CI, server-to-server)
Section titled “API-key (CLI, CI, server-to-server)”API keys are 64 hex characters prefixed with ak_live_. Sent the same
way: Authorization: Bearer ak_live_•••.
- Storage: only the SHA-256 hash and the masked prefix are persisted. The raw key is shown once on creation, never again.
- Scope: each key belongs to exactly one organization.
- Lifetime: keys never expire on their own — you control rotation.
- Revocation: immediate. Subsequent requests get
401 Invalid or revoked API key. - Audit:
last_used_atupdates on every successful auth (visible on the Settings → Security → API Keys row).
See API keys for the lifecycle workflow.
Email verification
Section titled “Email verification”Email/password signups must verify the address before a session is
issued. Verification links expire after 15 minutes
(EMAIL_VERIFICATION_EXPIRE_MINUTES).
Password storage
Section titled “Password storage”Bcrypt with a cost factor that bcrypt’s gensalt() chooses by
default. We never log passwords.
Organization isolation
Section titled “Organization isolation”Every authenticated request resolves to a user and an organization context. The org context comes from:
- The
ak_live_API key (each key maps to one org), or - The
X-Org-Idrequest header (web sessions can switch orgs), or - The user’s personal org as a fallback.
The middleware then verifies the user has an active membership in
that org. If not: 403 Not a member of this organization. If the
org doesn’t exist: 404.
From that point on, every database query in the request lifecycle
filters by org_id. Cross-org reads return 403.
Roles and permissions
Section titled “Roles and permissions”Four roles per org:
| Role | API keys | Schedules | Members | Tests |
|---|---|---|---|---|
| Owner | Full | Full | Full | Full |
| Admin | Full | Full | Full (except other admins) | Full |
| Member | View only | Run + edit | View only | Run + edit |
| Viewer | — | View only | View only | View only |
Role checks are enforced at the route layer using require_admin_or_owner,
permissions middleware, and the org context. See
Inviting team members for the
constraints (admin can’t change another admin, owner can’t be removed,
etc.).
Transport and headers
Section titled “Transport and headers”Every API response (regardless of route) carries:
X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-origin
API responses additionally get Cache-Control: no-store, no-cache, must-revalidate so credential-bearing data doesn’t sit in
intermediate caches.
CORS allows credentialed requests (cookies + Authorization header)
from origins explicitly listed in CORS_ORIGINS. Methods: GET,
POST, PUT, DELETE, OPTIONS. Headers: Authorization,
Content-Type, X-Requested-With, X-Org-Id. Star-CORS
(*) is never used.
Sessions
Section titled “Sessions”Active sessions surface under Settings → Security → Active Sessions. Each session row shows the device label and last-used time. Two actions:
- Revoke individual session — instantly invalidates that session’s tokens.
- Sign out everywhere — revokes every session except the current one.
Refreshing a token issues a new access/refresh pair against the same
underlying session record (the sid doesn’t change, but the session’s
last_used_at updates). If the session has been deleted in the
meantime, the refresh request is rejected with 401 Session has been revoked.
Secrets and configuration
Section titled “Secrets and configuration”- JWT_SECRET_KEY and SESSION_SECRET_KEY must be changed from
defaults in production. The settings model raises
ValueErroron startup ifENVIRONMENT=productionand either is still the placeholder. - API provider keys (
OPENAI_API_KEY,ANTHROPIC_API_KEY, etc.) are read from environment variables. They aren’t logged; we mask tokens after the first 8 characters in any log line that touches them. - Database URL uses connection pooling sized via
DATABASE_POOL_SIZE/DATABASE_MAX_OVERFLOW.
Webhook verification
Section titled “Webhook verification”The Stripe webhook endpoint exists at /api/v1/webhooks/stripe but
returns 503 today — Stripe isn’t wired up. When it ships, signature
verification (stripe.Webhook.construct_event) and event-id deduping
will be enforced on the handler. See
How to upgrade, downgrade, or cancel
for the current Mock Stripe stub.
What we don’t enforce yet
Section titled “What we don’t enforce yet”- MFA / 2FA — not in the codebase today.
- SAML SSO — Enterprise tier feature, planned. Google Workspace SSO
is on Team and Enterprise (
google_sso=Truein tier catalog). - Audit log API — Enterprise tier feature, planned (
audit_logsflag is set; the user-facing log isn’t shipped). - IP allowlists / geo-restrictions — not implemented.
- Per-key scoping — keys are org-wide, not endpoint-scoped or read-only. Rotate frequently if you’re embedding keys in shared CI environments.
Related
Section titled “Related”- Data handling — what’s stored, where it lives, what gets sent to AI providers
- API keys — creating, rotating, revoking
- Plan tier limits — which security features (SSO, audit logs) ship at which tier