Authentication
Create accounts, authenticate identities, and link ordinary provider JWTs.
A Jazz context always belongs to an account and acts as one exact identity. An identity is the pair of JWT claims iss (issuer) and sub (subject). An account is a stable ID that can admit several identities.
Create a session with configuration once:
import { createJazzSession } from "jazz-tools/client";
const jazz = await createJazzSession({
appId: "my-app",
serverUrl: "https://core.example",
initial: "local-first",
});The session restores a usable saved account or creates a local-first account when none exists. It owns graceful context replacement for subsequent auth commands. Local-first creation works offline once runtime assets are available.
Contexts accept an opaque AccountHandle, rather than raw JWTs or signing secrets. A handle fixes both the account and acting identity. The server still verifies credentials independently; copying an account ID into a JWT or object grants no access.
External authentication
Configure the Jazz core with your provider's signing keys, issuer, and audience. The provider can issue ordinary JWTs; it does not need to put a Jazz account ID or linking assertion in their claims.
Choose the operation that matches the user's intent:
| Operation | Result |
|---|---|
jazz.loginOrRegisterJWT(auth) | Atomically select an active assignment or create the account for a fresh identity. |
jazz.registerJWT(auth) | Create an account for a fresh external identity. |
jazz.loginJWT(auth) | Select an existing, active account assignment. |
jazz.linkJWT(auth) | Attach a fresh external identity to the currently selected account and select its new handle. |
jazz.getSnapshot().account | Read the locally selected handle, or undefined. |
jazz.logout() | Clear selection and invalidate this manager's issued credentials. |
Registration, login, and linking require a connection to the core. For ordinary provider signup, sign-in, and restoration, use loginOrRegisterJWT. The core performs one ordered decision: return the existing active assignment or create it when unassigned. Revoked identities remain rejected and assignments are never moved. Strict loginJWT and registerJWT remain available when the application needs those specific operations. An enrollment rejection preserves the previous selection. If enrollment succeeds but local persistence or client startup fails, the new selection remains; use jazz.retry() to retry that local work.
await jazz.loginOrRegisterJWT({
getToken: async () => {
const token = await getTokenFromYourProvider();
return token;
},
});
const { client } = jazz.getSnapshot();auth can also be a JWT string. Use a getToken callback for renewable credentials. The shared account context refreshes them across all framework bindings; there is no provider-level onJWTExpired callback. Every refreshed token must have the same exact issuer and subject. A callback that does not settle within 30 seconds fails that attempt; its eventual result cannot replace a later credential.
Automatic provider connections
For auth-required React apps, let JazzProvider own the session and its provider connection:
import { betterAuth } from "jazz-tools/client";
import { JazzProvider, useJazzAuth } from "jazz-tools/react";
function Root() {
return (
<JazzProvider
appId={APP_ID}
serverUrl={SERVER_URL}
auth={betterAuth(authClient)}
signedOut={<SignIn />}
>
<App />
</JazzProvider>
);
}
function SignOut() {
const auth = useJazzAuth();
return <button onClick={() => void auth.logout()}>Sign out</button>;
}Forms only call Better Auth's signUp and signIn. Jazz follows provider hydration and session changes, atomically logs in or registers, and renders the application only when its client is ready. The provider supplies loading and retryable error UI; signedOut supplies your sign-in screen. Logout flushes Jazz before revoking the provider session. Failures remain in the lifecycle state and its error UI.
useJazzAuth() exposes the shared lifecycle and its logout() and retry() actions. React Native and Expo use their corresponding Jazz imports and native fallback controls. Svelte, Solid and Vue use the same underlying lifecycle through their framework bindings.
For WorkOS or another JWT provider, use jwtAuth({ key, isPending, getToken, logout }). The key identifies the provider session or exact issuer/subject, never the rotating JWT string. The logout callback signs out of the provider or clears the application's token source; the source must then report key: null. Jazz waits for that acknowledgement before rendering signed-out content.
Vanilla applications can own the same observable lifecycle with createJazzApp({ appId, serverUrl, auth: betterAuth(authClient) }), subscribe to its snapshot and call dispose() on teardown. Without managed auth, the convenience owner defaults to a local-first account. Explicit initial configuration takes precedence. Existing low-level createJazzSession, connectBetterAuth and connectAuthProvider remain available for custom ownership.
Linking identities
An exact identity can belong to only one account, permanently. Revocation prevents its use but does not free it for reassignment. Linking therefore accepts only a fresh identity. If the identity already belongs to a different account, the app must decide how to migrate data; Jazz does not merge accounts.
For a hybrid app, omit managed auth and use the framework hook’s sessionActions, or own a low-level session directly. Signup calls the provider and then jazz.linkJWT(auth) on the retained guest account; ordinary login calls loginOrRegisterJWT. This ensures the fresh identity is linked before any automatic account creation.
jazz.linkJWT(auth) handles graceful shutdown, linking outside contexts, and opening the replacement client. See Lifecycle for the complete handoff and failure path.
The core orders a two-party protocol: the admitted identity authorizes an expiring nonce committed to the fresh identity, and the fresh identity authenticates acceptance. Both requests use ordinary bearer credentials. The core checks that the approver is still active and allowed to manage identities when acceptance happens.
Ownership and provenance
Public sessions and author columns expose the same structured value:
{
account: "00000000-0000-4000-8000-000000000001",
identity: { issuer: "https://issuer.example", subject: "alice" },
}Compare .account for account-owned data. Comparing the whole structure also compares issuer and subject. Two linked identities share account ownership while retaining distinct authorship.
Provider claims remain under session.claims. They cannot override session.user.account or the acting identity. See Sessions and Permissions.
Backend request contexts
Create a Node owner with await createJazzSession({ appId, app, permissions, driver, serverUrl, initial: { backendSecret } }) from jazz-tools/backend. Backend admission requires a live core check; the secret is private, ephemeral handle material. The ready snapshot's client.db performs backend work.
await client.forRequest(request) verifies the original bearer and resolves its active account through the core before opening an immutable requester scope. Configure JWT verification for external providers. By default, external identities must already be registered; local-first proofs can establish their deterministic founding account. Auth-required server routes can opt into client.forRequest(request, { account: "login-or-register" }) to atomically create a fresh external assignment after verifying the bearer. The option applies only to that immutable request scope; it never changes the backend account or accepts revoked identities. Keep the default strict mode on hybrid routes where a fresh identity must be linked first.
await client.forAccount(account) accepts an opaque admitted user handle and re-verifies its credential. Neither method switches the shared owner. Raw session objects cannot select an arbitrary principal or account. await client.withAttribution(account) retains backend permissions while recording verified user authorship.