Authentication
Every /v1 product endpoint is owner‑scoped: the server resolves the caller
to a single owner (an account) and keys every database read and write on that
owner. There is no way to address another account’s data by supplying an ID in a
body or query — the owner is taken from the verified credential, never from
request input.
Auth tiers
| Tier | Used by | Mechanism |
|---|---|---|
| Public | health, all /v1/public/* share surfaces, the webhook pages | none |
| Clerk actor | every /v1 product route | Authorization: Bearer <Clerk JWT> |
| Entitlement‑gated | uploads, sharing, search, AI, catalog‑analysis | Clerk actor + plan/feature check |
| Webhook signature | inbound webhooks | provider HMAC / signature header |
| Internal secret | /v1/internal/*, media‑intelligence callback | Authorization: Bearer <shared secret> |
The actor (requireSwayzioActor)
Authenticated routes resolve a SwayzioActorContext:
type SwayzioActorContext = {
actorId: string; // the authenticated user (Clerk subject)
ownerId: string; // the account everything is scoped to
ownerIdSource: string; // how ownerId was resolved (telemetry)
email?: string;
emails: string[];
scopes: string[];
};Resolution order (first match wins):
- Dev header
x-swayzio-user-id— non‑production only. SetsownerId === actorId, scopelocal:dev. Never honored whenNODE_ENV === "production". - Bearer JWT —
Authorization: Bearer <token>verified via ClerkverifyToken. - Clerk session via a configured JWT template (
CLERK_JWT_TEMPLATE). - Clerk
publicMetadata.swayzioLegacyOwnerId(cached ~5 min) — links a modern Clerk user to a legacy owner id. - Raw session claim —
AUTH_OWNER_CLAIM(defaultorg_id).
If no actor can be resolved, the endpoint returns 401 { "error": "unauthorized" }.
The dev header is a deliberate local‑development convenience. It is disabled in
production. The Fastify Core API enforces the same NODE_ENV !== "production"
gate (requireActor).
Owner‑scoping guarantee
- Handlers inject
ownerId: actor.ownerIdinto request schemas — they never read an owner from the body or query. - Repositories enforce
WHERE owner_id = $1on every query. - Responses strip
ownerId(and storage URIs, provider fields) viaclient*/public*DTO variants — see Data Contracts. - Shared resources (a pack shared with you, a library you collaborate on) are
reachable only through an accepted grant/share, graded server‑side
(
canEdit/canCollaborate/canAdmin). Public share links resolve a synthetic actor{ actorId: "public", ownerId: <link owner> }so signing stays owner‑scoped.
Entitlement gates
Some actions require a feature/plan entitlement on top of a valid actor. A denied
entitlement returns 403:
{
"error": "entitlement_required",
"code": "<feature_code>",
"feature": "<feature>",
"current": 3,
"limit": 3,
"planId": "<plan>",
"status": "<status>",
"upgradeRequired": true
}Gated features include packs.create, packs.share, tracks.share,
search.private, collaborators.invite, upload entitlements, and paid‑plan gates
on catalog analysis and the AI chat endpoints. Internal staff (verified
@swayzio.com email) are exempt from all gates.
Server‑to‑server secrets
Internal endpoints authenticate with a fixed bearer secret rather than a user
token. These fail closed — a missing secret env var yields 401, never an
open door.
| Surface | Secret env var |
|---|---|
POST /v1/internal/catalog-drain/*, /v1/internal/workflows/trigger-status | SWAYZIO_TRIGGER_CALLBACK_SECRET (or TRIGGER_STATUS_CALLBACK_SECRET) |
GET/POST /v1/internal/audio-embeddings/drain | CRON_SECRET |
POST /v1/media-intelligence/callback | MEDIA_INTELLIGENCE_CALLBACK_SECRET (+ contract‑version header) |
See Internal & Services and Webhooks.