Authentication
How and when to use Jazz's auth modes, and how to manage JWT auth over the lifetime of a live client.
Jazz has three auth modes: anonymous, local-first, and external. You pick the mode implicitly by what you pass in DbConfig: nothing for anonymous, secret for local-first, or jwtToken for external.
| Mode | What it does | When to use it |
|---|---|---|
anonymous | Ephemeral read-only identity, no secret, no server | Public/marketing surfaces, try-before-anything |
local-first | Stable identity from a device secret, no server needed | Production offline-first apps, try-before-signup |
external | Validates a JWT from your auth provider via JWKS or a configured static key | Production apps with real user accounts |
Anonymous sessions can subscribe to queries but are structurally denied writes — every insert, update, and delete path checks the session's auth mode before any policy evaluation runs, and surfaces an AnonymousWriteDeniedError to the client. Gate reads the same way you gate any other access, via the permissions DSL (session.where({ authMode: "anonymous" })).
Local-first auth
Local-first auth lets users start using your app without signing up. They can later upgrade to an external provider while keeping their identity. See Local-first auth for the full guide.
External auth for production
Pass a JWT from your auth provider using the jwtToken option. Jazz validates it against your server's configured JWKS endpoint or static key.
JWT? JWKS?
JWTs, or JSON Web Tokens, are small JSON payloads signed by an authority — normally this is your authentication provider. Often this JSON payload includes details about your identity, but it can also include various other pieces of information known as 'claims'. Jazz can read the signed payload and use it in your permissions policies. To validate the signature and protect against spoofing, Jazz either retrieves the signing key from the JWKS endpoint you specify or uses a single configured JWK / public key directly.
For more details, the Auth0 docs are a great starting point.
export function JwtAuthApp() {
return (
<JazzProvider
config={{
appId: "my-app",
serverUrl: "http://127.0.0.1:4200",
jwtToken: "<provider-jwt>",
}}
>
<TodoApp />
</JazzProvider>
);
}<script setup lang="ts">
import { createJazzClient, JazzProvider } from "jazz-tools/vue";
const client = createJazzClient({
appId: "my-app",
serverUrl: "http://127.0.0.1:4200",
jwtToken: "<provider-jwt>",
});
</script>
<template>
<JazzProvider :client="client">
<slot />
</JazzProvider>
</template><script lang="ts">
import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
const client = createJazzClient({
appId: 'my-app',
serverUrl: 'http://127.0.0.1:4200',
jwtToken: '<provider-jwt>',
});
</script>
<JazzSvelteProvider {client}>
{#snippet children({ db })}
<slot />
{/snippet}
</JazzSvelteProvider>export async function createJwtDb() {
return createDb({
appId: "my-app",
serverUrl: "http://127.0.0.1:4200",
jwtToken: "<provider-jwt>",
});
}Use jwtToken in JazzProvider or createDb(...) for initial setup and whenever the signed-in
user changes. Recreate the client for sign-in, sign-out, or principal switches. Reserve
db.updateAuthToken(...) for refreshing a JWT for the same user on an already-authenticated
client.
Managing JWT changes on a live client
For sign-in and sign-out, recreate JazzProvider or Db with a new auth config:
const [jwtToken, setJwtToken] = useState<string | null>(readStoredJwt());
const { secret, isLoading } = useLocalFirstAuth();
if (!jwtToken && (isLoading || !secret)) return <p>Loading…</p>;
const config = jwtToken
? {
appId: "my-app",
serverUrl: "https://sync.example.com",
jwtToken,
}
: {
appId: "my-app",
serverUrl: "https://sync.example.com",
secret: secret!,
};
<JazzProvider key={jwtToken ? "external" : "local"} config={config} />;On a live client, db.updateAuthToken(jwt) is only for replacing the bearer token with a new JWT for
the same principal:
db.updateAuthToken(jwt)refreshes bearer auth for the same principal on the live client.- Backend-scoped wrappers from
createJazzContext().asBackend(),.forRequest(), and.forSession()do not own shared bearer auth and ignoreupdateAuthToken(...). - Do not use
db.updateAuthToken(null)for logout or local-first fallback on a live client. - Switching from one principal to another, like
alice -> bob, is not supported on a liveDb. RecreateDborJazzProviderwhen the authenticated user changes.
If you change the config passed to JazzProvider, Jazz recreates the client. That is the
recommended path for login, logout, and any principal change.
Reacting to expiry and unauthenticated responses
Auth state is flat:
interface AuthState {
authMode: "anonymous" | "local-first" | "external";
session: Session | null;
error?: "expired" | "missing" | "invalid" | "disabled";
}Presence of error indicates trouble; authMode and the last-known session are always available.
React: onJWTExpired prop
For JWT refresh, pass onJWTExpired to JazzProvider. Jazz calls it when the sync server rejects
the current token as expired, serialized so concurrent failed writes trigger a single refresh:
<JazzProvider
config={{ appId: "my-app", jwtToken }}
onJWTExpired={async () => await fetchFreshJwt()}
>
<App />
</JazzProvider>Read auth state from any component with useAuthState():
const { authMode, userId, claims, error } = useAuthState();TypeScript: db.onAuthChanged
Outside React, subscribe to auth-state changes on Db:
const stop = db.onAuthChanged(async (state) => {
if (state.error !== "expired") return;
const freshJwt = await getFreshJwtForCurrentUser().catch(() => null);
if (freshJwt) {
db.updateAuthToken(freshJwt);
return;
}
// Sign-out or principal change: recreate Jazz without bearer auth.
setJwtToken(null);
});Read the current snapshot synchronously with db.getAuthState().
When Jazz receives a structured unauthenticated response from the sync server, it:
- sets
erroron the auth state - preserves the last known
session - pauses authenticated sync and reconnects until the app either refreshes the same user's JWT with
db.updateAuthToken(...)or recreates the client with a new auth config
useSession() and db.getAuthState().session can still return the last known session while
error is set. That's intentional: it keeps user-scoped UI and local authorship stable while your
app refreshes or replaces the JWT. For signed-in vs signed-out UI, check whether error is set
(or the authMode you expect) instead of only whether a session exists.
Server configuration
See Server Setup for the full list of server flags and storage options.
NODE_ENV=production \
jazz-tools server "$JAZZ_APP_ID" \
--port 1625 \
--data-dir ./data \
--allow-local-first-auth \
--jwks-url https://auth.example.com/.well-known/jwks.json \
--backend-secret "$JAZZ_BACKEND_SECRET" \
--admin-secret "$JAZZ_ADMIN_SECRET"Session resolution
When the server receives a request, it tries each auth method in priority order and uses the first match:
- Backend impersonation — the request includes
X-Jazz-Backend-SecretandX-Jazz-Sessionheaders. A trusted backend service (e.g. a cron job or webhook handler) can act as any user by providing the shared secret and the session as a base64-encoded JSON string inX-Jazz-Session. - External JWT — the request includes an
Authorization: Bearer <jwt>header. The token is validated against the configured--jwks-urlor--jwt-public-key. - Local-first auth — the request includes an
Authorization: Bearer <jwt>header with a self-signed local-first token. - No session — none of the above matched.
Backend impersonation always takes precedence, so a backend service can reliably impersonate users even if the request also carries a JWT.
On TypeScript backends, await createJazzContext(...).forRequest(req) follows the same rule set. Configure jwksUrl or jwtPublicKey on the backend context to verify external JWTs there too, but not both.
Upgrading to external auth
Users on local-first auth can sign up with an external provider and keep the same identity. See Signing up with BetterAuth for the full walkthrough.
Next steps
- Sessions — read the current user and scope queries to their identity
- Permissions — define row-level access policies