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

  1. The user signs in with your auth provider and gets a JWT.
  2. Your client passes that JWT to Jazz.
  3. The Jazz server validates the JWT signature against the provider's JWKS endpoint.
  4. On success, Jazz uses the JWT's sub claim as session.user_id.

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 }) => ({
          claims: { 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

Get the JWT from Better Auth and pass it to Jazz via the config prop.

App.tsx
export function App() {
  const { data: session, isPending } = authClient.useSession();
  const [token, setToken] = useState<string | undefined>();

  useEffect(() => {
    if (isPending || !session?.session) {
      setToken(undefined);
      return;
    }

    authClient.token().then((res) => {
      if (res.error) return;
      setToken(res.data.token);
    });
  }, [isPending, session?.session?.id]);

  return (
    <JazzProvider
      config={{
        appId: "my-app",
        serverUrl: "wss://your-jazz-server.example.com",
        jwtToken: token,
      }}
    >
      <YourApp />
    </JazzProvider>
  );
}

Jazz server configuration

Point the Jazz server at your Better Auth JWKS endpoint:

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

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, getAccessToken } = useAuth();
  const [token, setToken] = useState<string | undefined>();

  useEffect(() => {
    if (!user) {
      setToken(undefined);
      return;
    }

    getAccessToken().then((accessToken) => {
      setToken(accessToken ?? undefined);
    });
  }, [getAccessToken, user]);

  return (
    <JazzProvider
      config={{
        appId: "my-app",
        serverUrl: "wss://your-jazz-server.example.com",
        jwtToken: token,
      }}
    >
      <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...

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_id }, 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 uses the JWT sub claim as session.user_id. If your provider keeps sub fixed to its own user ID, mint the JWT with sub set to the Jazz user ID yourself — either via a custom getSubject hook on the provider or by issuing the JWT directly from your server.

Full working examples: Better Auth chat, WorkOS chat.

On this page