Four credentials, four keys
Dashboard session JWT signed with JWT_SECRET developer, whole account
Project API key bcrypt + pepper, never stored raw backend, whole project + one environment
Chat token signed with CHAT_TOKEN_SECRET one user, short-lived
RTC token signed with the SFU's own secret one participant, one room
None can mint or impersonate another. Chat tokens carry a fixed
aud: "raven-chat", so even a shared signing key wouldn't let a
dashboard session be replayed on the chat plane.
Production refuses to start if CHAT_TOKEN_SECRET is unset or equal to
JWT_SECRET. Local development falls back so a fresh clone still runs;
production does not.
The identity chokepoint
Every write derives its actor from the signed credential, never from the request body:
export function resolveSubjectId(actor: ChatActor, requested?: string | null) {
if (actor.kind === 'client') return actor.userId; // body ignored entirely
return requested ?? actor.userId;
}A browser token passing senderId: 'someone-else' isn't an error — the
value is silently discarded. Server actors can name a user, because
that's how a backend posts on someone's behalf, and it's exactly why an
API key must never reach a browser. See
Authentication.
Authorization is two independent checks
What the token allows — scopes, narrowed from a role, never widened. What you're actually a member of — checked server-side on every request, never trusted from a client-cached list. Both have to pass. See Chat → Authorization and Roles & Permissions for the control-plane equivalent.
Secrets
- An API key's secret half is bcrypt-hashed with a server-side pepper — never stored raw, never recoverable, shown to you exactly once.
- A webhook signing secret is stored (delivery signing requires it) but never returned again after creation.
- Server SDKs keep the API key in a private field — never enumerable,
never in
JSON.stringify()/repr(), never logged, never in a thrown error. See Node.js SDK and Python SDK.
Attachments
Storage credentials never reach the browser — only a signed URL scoped to one object, for a few minutes. Storage keys are random, never the user-supplied filename, closing the obvious path-traversal attempt outright rather than sanitizing it. See Attachments.
What's a known limitation, stated rather than implied
Webhook SSRF protection is hostname-level only — it refuses obvious internal targets but doesn't resolve DNS, so a hostname that resolves to a private IP still gets through. A production deployment should egress-filter the delivery worker at the network level. See Webhooks → Security.
Reporting a vulnerability
Reach the team on Discord, or your account contact if you have one — Raven's source and issue tracker aren't public, so a security report isn't something to post in the open.