The model in one paragraph
A caller proves identity with one of two credentials, and selects an organization with a second, orthogonal value:- Clerk JWT — the bearer token humans carry through the web app and the realtime server. Verified against Clerk’s JWKS.
- API key (
mx_live_*) — a long-lived secret that programmatic callers and the SDKs send. Verified by a one-way fingerprint lookup using a server-side secret.
mx_live_ is an API key, anything else is a Clerk JWT. Organization scope rides separately — as the X-Organization-ID HTTP header on REST and SDK calls, and as an organizationId field in the Socket.io handshake on the realtime plane. Identity and org scope are independent axes; you almost always need both.
The two credential types
Clerk JWT
Authorization: Bearer <jwt>. Verified against Clerk’s JWKS. On first use the user is provisioned just in time.API key (mx_live_*)
Authorization: Bearer mx_live_… (or X-API-KEY: mx_live_…). Verified by a one-way fingerprint lookup. Rate-limited per key and per user.How the backend resolves a credential
Every authenticated route runs the same resolver, which inspects headers in a fixed order and dispatches by token prefix.Authorization: Bearer is checked first
mx_live_, it is treated as an API key. Otherwise it is treated as a Clerk JWT.X-API-KEY is the fallback
mx_live_.Neither present is a 401
401 and the message Authentication required. Provide Authorization: Bearer token or X-API-KEY header.Clerk JWT path
Clerk JWTs are verified, not minted, by ModuleX. The verifier returns the token’s claims on success and rejects otherwise.Verification
Configuration is required
CLERK_JWKS_URL and CLERK_ISSUER. If either is unset, verification returns no claims and the request is rejected.The signing key is fetched from JWKS
kid header selects the matching key; no match means rejection.The RS signature and issuer are checked
issuer is checked.Claims are read
sub (Clerk user id), email, azp, and sid.user_2abc123xyz. Missing → 401 with User ID missing in token.401 with Email not found in token claims.clerk, the effective role is taken from this claim (upper-cased) rather than the stored database role.Just-in-time provisioning
On the first valid JWT for an unknown user, the backend creates the account in a single transaction: aUser (with the user-level role USER), a personal default organization, owner membership of that org, and seeded defaults (a starter workflow, managed-auth credentials, and a default Composer model). The operation is idempotent and concurrency-safe, and pending invitations are linked afterward.
POST /api-keys) requires an authenticated, already-provisioned user, and accepts either a Clerk JWT or an existing mx_live_* API key — so an existing key can mint more keys. Because only a JWT provisions a user just in time, your first key necessarily needs a JWT. You bootstrap programmatic access by signing in to the app once (which provisions the user), then minting a key.API key path (mx_live_*)
API keys are the programmatic credential. They are minted from an authenticated request (a Clerk JWT or an existing API key), returned in full exactly once, and stored only as a salted hash.
Format and crypto
mx_live_.mx_live_ followed by ~43 Base62 characters.mx_live_{hint}********.API_KEY_PEPPER (minimum 32 characters), is required in production and staging — the app exits at startup if it is missing or too short. Because that secret lives outside the database and is folded into each fingerprint, the database alone cannot be turned back into plaintext keys. See Data security & encryption for how credential secrets are protected.What a key carries
null, it works across all of the owner’s organizations (you still pass X-Organization-ID per request).null means the key never expires.last_used_ip.What happens on each API-key request
Hash and look up
401 Invalid API key (and the failure is recorded for security monitoring).Check expiry
401.Enforce rate limits
429 with X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers.Load the user and stamp usage
last_used_at and last_used_ip are updated.Attach org scope if pinned
X-Organization-ID header can be validated against it on org-bound routes.Authenticated calls, three ways
The same authenticated call — fetch the current user, then make an org-scoped call — shown with cURL, Python, and JavaScript. Auth is alwaysAuthorization: Bearer mx_live_… plus X-Organization-ID on org-bound routes.
MODULEX_API_KEY, MODULEX_ORGANIZATION_ID, and MODULEX_BASE_URL when arguments are omitted. The JavaScript SDK has no environment fallback — you must pass apiKey and organizationId explicitly. See JavaScript SDK and Python SDK.API keys are tied to your account
You manage keys from the app’s API key settings, where each key is shown in full only at the moment you create it.Realtime: the Socket.io handshake
The realtime collaboration server does not use HTTP auth headers. It authenticates once, at the Socket.io handshake, reading both the token and the organization id from theauth payload. Both fields are required, and either missing or failing rejects the entire connection.
mx_live_* API keys. Org scope here travels in the handshake auth.organizationId field, not as the X-Organization-ID header used on REST and SDK calls.connect_error (a handshake rejection), not as an error event payload:
connect_error with Authentication required. Failing verification (bad signature, wrong issuer) → connect_error with Invalid token.connect_error with Organization ID required. Not a member of that org → connect_error with Not a member of this organization.user:{userId} and org:{organizationId} rooms. See Socket.io collaboration events and Realtime overview for what flows over the connection.
Roles are a third, separate concern
Identity (who you are) and org scope (which org you act in) are distinct from role (what you may do in that org). After membership is confirmed, the live roles areowner and admin.
Platform and admin keys (not your keys)
For completeness: ModuleX operations staff use credentials that are notmx_live_* user keys and that you never handle.
- A shared admin dashboard key gates the internal platform dashboard. Presented as
X-Admin-API-KeyorAuthorization: Bearer, compared constant-time against a rotation list, and it fails closed —503when unconfigured,401when missing,403on mismatch. - A separate super-admin gate requires a user-level
SUPER_ADMINrole and a@modulex.devemail. - The realtime server has its own admin gate (a configured admin key plus a Clerk token and a single allowed email).
MODULEX_API_KEY scheme has been removed. Never conflate these platform credentials with your mx_live_* keys.
Edge cases and gotchas
There is no /auth/login, /register, /logout, or /refresh
There is no /auth/login, /register, /logout, or /refresh
/auth/login is vestigial and hits no route.X-API-KEY works against the backend but no SDK sends it
X-API-KEY works against the backend but no SDK sends it
X-API-KEY: mx_live_… as an alternative to Authorization: Bearer. Both SDKs use Authorization: Bearer exclusively. A self-hosted or hand-rolled caller may use X-API-KEY directly if preferred.A request body's organizationId is not the X-Organization-ID header
A request body's organizationId is not the X-Organization-ID header
organizationId field that sets the resource owner (for example, which org a new API key belongs to). That is independent of the X-Organization-ID header, which selects the org you are acting in. Do not conflate the two.Header casing is exact on the wire
Header casing is exact on the wire
X-Organization-ID, X-API-KEY, X-Admin-API-Key, and the X-RateLimit-* family. Response bodies are snake_case (organization_ids, primary_organization_id, current_organization_id). HTTP header lookup is case-insensitive, but quote these exact casings.The full API key is shown exactly once
The full API key is shown exactly once
POST /api-keys is the only response that returns the plaintext key. Every later read returns the masked form mx_live_{hint}********. Store the key securely at creation; it cannot be recovered.Web app vs API key — which to use
Web app vs API key — which to use
mx_live_* API key for servers, scripts, CI, and the SDKs. To get your first key, sign in once (which provisions your account), then mint a key from API key settings.