Auth

Auth provider integration

Connect an external auth provider to Jazz with JWT validation, with examples for Better Auth and WorkOS.

Jazz supports external JWT-based authentication for production use. This recipe walks through connecting a provider to Jazz, with examples for Better Auth (self-hosted) and WorkOS (managed service).

How it works

Provider sign-in

An external JWT provider connecting to a Jazz server.

Sign inJWT tokenRegister or log inwith JWTFetch JWKSPublic keysVerify JWT andresolve accountAccount handlereadyBrowserAuth providerJazz server
  1. The user signs in with your auth provider and gets a JWT.
  2. The shared provider connection calls loginOrRegisterJWT({ getToken }).
  3. The Jazz server validates the JWT signature against the provider's JWKS endpoint.
  4. Core atomically resolves the exact (iss, sub) assignment or creates its account when fresh. The Jazz session opens the account's client. session.user contains the account ID and exact acting identity; provider claims remain under session.claims. Revoked assignments remain rejected; login-or-register never reassigns an identity.

If you're unfamiliar with JWTs and JWKS, see the explainer on the Authentication page.

Provider setup

Better Auth is a self-hosted auth framework. You run the server yourself and enable the jwt plugin, which exposes a JWKS endpoint and issues signed JWTs.

Server

src/lib/auth.ts
export const auth = betterAuth({
  // your database, email config, etc.
  plugins: [
    jwt({
      jwks: {
        keyPairConfig: { alg: "ES256" },
      },
      jwt: {
        issuer: "https://your-app.example.com",
        definePayload: ({ user }) => ({ role: (user as { role?: string }).role ?? "" }),
      },
    }),
  ],
});

This exposes a JWKS endpoint at /api/auth/jwks (or wherever you mount Better Auth's handler).

Client

Create the auth client with the jwtClient plugin so you can request JWT tokens.

src/lib/auth-client.ts
export const authClient = createAuthClient({
  plugins: [jwtClient()],
});

Connecting to Jazz

Pass auth={betterAuth(authClient)} to JazzProvider. Forms only call Better Auth signup or sign-in; Jazz owns startup, account changes and error recovery. Use useJazzAuth().logout() to flush Jazz before revoking the provider session.

App.tsx
export function App() {
  return (
    <JazzProvider
      appId="my-app"
      serverUrl="wss://your-jazz-server.example.com"
      auth={betterAuth(authClient)}
      signedOut={<p>Sign in to continue.</p>}
    >
      <YourApp />
    </JazzProvider>
  );
}
// Forms only call authClient.signUp / signIn. Jazz follows automatically.

Jazz server configuration

Configure the reachable JWKS endpoint and the exact issuer and audience emitted by Better Auth:

pnpm dlx jazz-tools@alpha server <APP_ID> \
  --jwks-url https://your-app.example.com/api/auth/jwks \
  --jwt-issuer https://your-app.example.com \
  --jwt-audience https://your-app.example.com

Better Auth starters use APP_ORIGIN for baseURL, JWT issuer/audience, and the Jazz server's jwksUrl, jwtIssuer, and jwtAudience. Set it consistently in the environment shared by the app and auth server. The React/Vite and TypeScript starters default to the auth server origin (http://localhost:3001), not the frontend's port 5173. A JWKS URL alone is not enough: missing or mismatched issuer/audience settings cause account login to return invalid account credential, even when Better Auth sign-in succeeds. Jazz Cloud must be able to reach your JWKS endpoint; a local localhost URL is not reachable from the cloud.

For a full working example, see the Better Auth chat example.

If your users start unauthenticated and sign up later, see Local-first auth for how to preserve their identity across the transition.

WorkOS is a managed auth service — no server-side auth code needed. Wrap your app with AuthKitProvider, get the access token, and pass it to Jazz.

App.tsx
function JazzWithWorkOS() {
  const { user, isLoading, getAccessToken, signIn, signOut } = useAuth();
  return (
    <JazzProvider
      appId="my-app"
      serverUrl="wss://your-jazz-server.example.com"
      auth={jwtAuth({
        key: user?.id ?? null,
        isPending: isLoading,
        getToken: () => getAccessToken({ forceRefresh: true }),
        logout: () => signOut(),
      })}
      signedOut={<button onClick={() => void signIn()}>Sign in</button>}
    >
      <YourApp />
    </JazzProvider>
  );
}
export function App() {
  return (
    <AuthKitProvider clientId="client_01ABC...">
      <JazzWithWorkOS />
    </AuthKitProvider>
  );
}

Jazz server configuration

Point the Jazz server at the WorkOS JWKS endpoint:

pnpm dlx jazz-tools@alpha server <APP_ID> \
  --jwks-url https://api.workos.com/sso/jwks/client_01ABC... \
  --jwt-issuer <JWT_ISSUER> --jwt-audience <JWT_AUDIENCE>

For a full working example, see the WorkOS chat example.

See Server setup for the full set of server flags.

Using JWT claims in permissions

Your auth provider's JWT may include custom claims (roles, organisation IDs, etc.). Access them in permissions via session.where(...).

permissions.ts
s.definePermissions(exampleApp, ({ policy, anyOf, session }) => {
  policy.todos.allowRead.where(
    anyOf([{ owner_id: session.user.account }, session.where({ "claims.role": "manager" })]),
  );
});

See Permissions for the full claims API.

Other providers

Any provider that issues JWTs and exposes a JWKS endpoint will work. The key pattern is always the same: get a JWT from your provider, pass it to Jazz.

ProviderJWKS endpointsub claim format
Better Auth<baseURL>/api/auth/jwksUser ID
WorkOShttps://api.workos.com/sso/jwks/<clientId>user_<id>
Clerkhttps://<app>.clerk.accounts.dev/.well-known/jwks.jsonuser_<id>
Auth0https://<tenant>.auth0.com/.well-known/jwks.jsonauth0|<id>
Firebasehttps://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.comFirebase UID

Jazz records the exact JWT iss and sub pair as the acting identity. Providers keep their existing subjects. Use linkJWT to associate a fresh identity with an existing account; use application rows for profile data.

Full working examples: Better Auth chat, WorkOS chat.

On this page