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
- The user signs in with your auth provider and gets a JWT.
- Your client passes that JWT to Jazz.
- The Jazz server validates the JWT signature against the provider's JWKS endpoint.
- On success, Jazz uses the JWT's
subclaim assession.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
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.
export const authClient = createAuthClient({
plugins: [jwtClient()],
});Connecting to Jazz
Get the JWT from Better Auth and pass it to Jazz via the config prop.
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/jwksFor 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.
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(...).
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.
| Provider | JWKS endpoint | sub claim format |
|---|---|---|
| Better Auth | <baseURL>/api/auth/jwks | User ID |
| WorkOS | https://api.workos.com/sso/jwks/<clientId> | user_<id> |
| Clerk | https://<app>.clerk.accounts.dev/.well-known/jwks.json | user_<id> |
| Auth0 | https://<tenant>.auth0.com/.well-known/jwks.json | auth0|<id> |
| Firebase | https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com | Firebase 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.