Security model
Read this before you go live. It explains what each credential can do, how we protect it, and what you must do on your side.
Tenancy
Every record (persona, knowledge, conversations, keys, settings, subclients) belongs to exactly one client and every query is scoped to it. A credential for one client can never read or write another's data:
- Whop: the member's Whop token is verified against Whop's keys on every request, and we ask Whop whether that user can access the exact experience. The business is read from Whop, never from the request.
- Dashboard: your session must belong to an email on that business's team, checked on every request.
- API keys and sessions: the client is derived from the key or token, never from the request body.
Credentials and how they're stored
| Credential | Format | Lifetime | Storage |
|---|---|---|---|
| Secret key | sk_live_ + 32 random characters (~190 bits) | Until revoked | SHA-256 hash only |
| Publishable key | pk_live_ + 32 | Until revoked | SHA-256 hash only |
| Chat session | cs_ + 32 | ≤ 24 h (anonymous 2 h) | SHA-256 hash only |
| Dashboard session | ses_ + 32, HttpOnly cookie | 30 days, revoked on sign-out | SHA-256 hash only |
| Sign-in link | ml_ + 32 | 15 min, single use | SHA-256 hash only |
Full keys are shown once, at creation. A copy of our database alone gives an attacker no usable credential.
What each key can do
- Secret key: create and end chat sessions for any user id you send. It's equivalent to "log in as any of my users" for the chat, so treat it like a database password: server-side only, in a secret store, never in front-end code, mobile apps or git. Requests carrying a browser
Originheader are refused. - Publishable key: start anonymous sessions only, only while anonymous chat is on, only from your allowed sites, rate limited. It can't impersonate a signed-in user or read anything.
- Chat session: chat as one subclient of one client until it expires. It can't list or read other conversations, knowledge or settings.
Getting a token to the browser safely
- Your server calls
POST /api/v1/sessionsafter your own login check, and returns only thecs_token to that user. - The widget passes the token to the chat frame with
postMessage, addressed to our exact origin. The frame accepts tokens only from your allowed sites. - Hosted links carry the token in the URL fragment, which is never sent to servers or included in
Referer, and the page removes it from the address bar. - Tokens are never placed in URL query strings, cookies on your domain, or logs.
Framing and origins
/chat/<client>sendsContent-Security-Policy: frame-ancestors 'self' <your allowed sites>, so no other website can frame your chat (clickjacking, quota theft).- The dashboard can only be framed by Whop. Other pages can't be framed at all.
- Cross-origin API calls are only answered with CORS headers for your allowed sites.
Dashboard protections
- Passwordless sign-in, so there are no passwords to leak or reuse.
- The session cookie is
HttpOnly,Secure,SameSite=Lax; scripts can't read it. - Every state-changing dashboard request must come from the same origin (CSRF protection), on top of
SameSite. - Sign-in responses are identical whether or not an email has an account, and are rate limited per IP and per email.
Rate limits and abuse
- Anonymous sessions: 20 per IP per hour, 2,000 per client per hour.
- Sign-in emails: 10 per IP and 5 per email per hour.
- Replies: your plan's monthly allowance, enforced atomically, so bursts can't exceed it.
Integrations
Connected apps (Fireflies, Drive, Notion…) are reached through Composio with read-only tools; we never store their tokens. Their content reaches subclients only when you import it into knowledge, or for the few apps you explicitly switch on for live use.
Your checklist
- Secret keys only in server environment variables or a secret manager.
- Your session endpoint requires your own login and uses your user id for
subclient.id, never a value from the request. - Allowed sites list only origins you control, all
https://. - Anonymous chat on only if you want public visitors.
- On sign-out,
Persona.destroy()in the page andDELETE /api/v1/sessionson the server. - Revoke and replace a key immediately if it may have leaked (Embed and API, API keys). Revocation is instant.
- Short
expires_infor sensitive apps; the widget renews automatically.
Reporting a vulnerability
Email security issues to the team that runs your Persona account. Please don't test against other clients' data.