Authentication

Magic-link sign-in for humans; Bearer tokens for machines.

Ventus Sessions has two authentication surfaces, deliberately separated.

The dashboard at dashboard.sessions.ventus.ai accepts only a magic link. Enter your email, click the link in your inbox, and you're signed in. Behind the scenes:

(one-time password) flow. Email goes through our own AWS SES SMTP so we control deliverability and never leak Supabase's default rate limits.

  • The link itself is a short-lived (60 s) PKCE token. Click expires it.
  • After exchange we set two httpOnly, Secure, SameSite=Lax cookies

that carry a short-lived (1 h) access token and a refresh token.

  • No password is stored, anywhere, ever.

Sign-up is invitation-only. New users are added by:

  1. A workspace owner sending an invitation from Members → Invite, or
  2. An organization admin adding an email to the bootstrap admin list at

deploy time (dashboard.api.bootstrapAdminEmails in Helm values).

Machines: API keys

Programmatic access uses a workspace-scoped API key, sent as Authorization: Bearer <key>. Keys:

  • Are created from the dashboard (API keys → Create key).
  • Are revealed exactly once. You see the raw key in the success

modal and then we only store a hash. If you lose it, mint a new one and revoke the old.

  • Identify the key record server-side via the ULID embedded in the

key; there's no separate "tenant ID" header.

  • Can be revoked from the same screen. Revocation is immediate on the

control plane.

export VENTUS_API_KEY=vs_01JC1AMQX4N3PWV9MR2BCKDH7E.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
curl -H "Authorization: Bearer $VENTUS_API_KEY" \
  https://api.sessions.ventus.ai/v1/health

Key format

vs_<ulid>.<32-char secret>

The prefix-and-ULID part is visible in the API keys list ("masked prefix") — everything left of the dot. That lets you identify which key did what in the audit log without ever needing to reveal the secret.

Scope and rate limits

  • Scope: one workspace. A key from Workspace A cannot list, create,

or audit anything in Workspace B.

  • Rate limit: 600 requests / minute / key by default. Override per

customer with a support ticket. 429 Too Many Requests includes Retry-After.

  • Audit: every authenticated call records the key's masked prefix,

the action, the timestamp, and (for sessions) the resulting ID. See Audit.

Internal tokens (sessions / live-view)

When you create a session you get back two signed URLs:

  • connect_url — a wss://…/cdp?token=eyJ… JWT good for the lifetime

of the session (max 30 min).

  • live_view_url — a https://view.…/?token=eyJ… JWT good for ~5 min.

These are session-scoped JWTs, not API keys. Don't put them in CI secret stores or commit them — they expire. If you need a long-lived viewing session, the right shape is a programmatic call to POST /v1/sessions/{id}:keepalive from your backend that re-mints the JWT on demand.

Threat model summary

AssetAuthLifetimeWhere stored
Dashboard sessionMagic-link → httpOnly cookie1 h, refreshBrowser cookie jar
API accessBearer token (workspace-scoped)Revocable, no expiryCustomer secret store
connect_url JWTSession JWTSession lifetime (≤30 m)In-memory, single-use
live_view_url JWTSession JWT5 minURL the human pastes

If you ever see a long-lived JWT in our system surfaced where an API-key-shaped credential would do the job, file a bug.