Authentication
Magic-link sign-in for humans; Bearer tokens for machines.
Ventus Sessions has two authentication surfaces, deliberately separated.
Humans: magic-link sign-in
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:
- We use Supabase Auth with the OTP
(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=Laxcookies
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:
- A workspace owner sending an invitation from Members → Invite, or
- 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/healthKey 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— awss://…/cdp?token=eyJ…JWT good for the lifetime
of the session (max 30 min).
live_view_url— ahttps://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
| Asset | Auth | Lifetime | Where stored |
|---|---|---|---|
| Dashboard session | Magic-link → httpOnly cookie | 1 h, refresh | Browser cookie jar |
| API access | Bearer token (workspace-scoped) | Revocable, no expiry | Customer secret store |
connect_url JWT | Session JWT | Session lifetime (≤30 m) | In-memory, single-use |
live_view_url JWT | Session JWT | 5 min | URL 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.