Internally each path sets
c.var.authKind to api_key | jwt | oauth. The usage-cap middleware meters agent traffic (api_key + oauth) but exempts admin traffic (jwt) — so a customer over their cap can still cancel/downgrade.
1. API keys (sk_live_…)
Long-lived. Mint via POST /api-keys; only the first 16 chars (sk_live_xxxxxxxx) are stored as a non-secret lookup prefix and the rest is argon2-hashed. Full key is shown ONCE. Lose it → revoke (DELETE /api-keys/:id) and create a new one.
key field is the only place the full secret appears):
scopes: ["*"] (full access). Pass read_only: true when creating a key to get scopes: ["read"] — it can read but every write returns 403 insufficient_scope. Finer per-resource scoping may come later.
API keys also work in MCP clients (Claude Desktop, Cursor) via mcp-remote --header — useful when you want to skip the OAuth browser flow during dev or in scripted setups.
Revoke:
204. Revoked keys immediately fail auth with 401 invalid_api_key. The row is kept for audit (revoked_at set).
2. Supabase JWTs (admin UI)
When a human signs up at/signup, verifies email, and logs in, the apps/web frontend sets Supabase Auth cookies. Every request from the web admin (/api, /records, /pricing) sends the access JWT to the API in the same Authorization: Bearer header.
JWT requests authenticate the user, then look up workspace_members to resolve their workspace.
The special-cased path POST /workspaces/bootstrap is the only endpoint a JWT-auth’d user can hit before having a workspace — it idempotently creates one and mints the first sk_live_ key. The admin /api page calls this on every load so a fresh signup lands straight on a usable workspace.
JWT-auth’d requests are exempt from the usage cap (so customers who hit their cap can still raise it or cancel) but still subject to the per-workspace rate limit.
Accepted signup emails
Signup rejects disposable and temporary mailboxes (Mailinator, Guerrilla Mail, 10 Minute Mail, and ~121k others, including wildcard subdomains like*.33mail.com) along with domains that cannot receive mail at all — example.com and the reserved TLDs .test, .example, .invalid, .localhost, .local.
Ordinary free providers are fine: Gmail, Outlook, Yahoo, iCloud, and Proton all work. You do not need a company domain.
The check runs twice — once on the signup form for an immediate error, and again on POST /workspaces/bootstrap, which returns 403 email_domain_not_allowed. Only new workspaces are checked, so an existing workspace is never affected if its domain is later added to the list. If a legitimate domain is being rejected, email support and it will be allowlisted.
3. OAuth 2.1 + PKCE (MCP clients)
MCP clients (Claude.ai, Cursor, ChatGPT, Windsurf, Claude Desktop) discover Salty’s auth server via RFC 9728 protected-resource metadata, then run the standard OAuth 2.1 + PKCE authorization-code flow:- Client GETs
/.well-known/oauth-protected-resource(from MCP server’sWWW-Authenticateheader). - Client GETs
/.well-known/oauth-authorization-server(RFC 8414 metadata). - Client POSTs
/oauth/register(RFC 7591 dynamic client registration) → gets aclient_id(public PKCE client, no secret). - Client redirects user’s browser to
/oauth/authorize?client_id=…&redirect_uri=…&code_challenge=…&state=…. - Salty 302-redirects to
/oauth/consentonapps/webwhere the user clicks Approve. - The browser returns to the client’s
redirect_uriwith?code=…&state=…. - Client POSTs
/oauth/tokenwith thecode+code_verifier→ gets asalty_oat_…access token + refresh token.
grant_type=refresh_token). Reusing an already-rotated refresh token is treated as a theft signal and revokes the entire token family — every access + refresh token minted from that authorization dies at once.
Salty’s OAuth endpoints are at:
Passwordless sign-in with passkeys
Admin accounts can register a passkey (WebAuthn — Touch ID, Windows Hello, or a hardware security key) from the dashboard Account page, then sign in with it instead of a password. The login screen offers the passkey automatically (with an explicit Sign in with a passkey button as a fallback), and new accounts are prompted to add one right after signup. A passkey is phishing-resistant and counts as strong authentication on its own, so a passkey sign-in is not additionally challenged for a TOTP code. (Passkeys are bound to the production origin — they work ontrysalty.com, not on localhost.)