Service-to-service auth (JWKS)
Verify auth tokens in your backend services and Cloudflare Workers without calling the auth-service on every request.
When JWKS is enabled, your bao auth-service issues a signed JWT on every sign-in alongside the session cookie. Backend services can verify that JWT locally using the public keys exposed at the JWKS endpoint — no round-trip to the auth-service required per request.
This is the right approach when:
- You have multiple services or Workers that need to verify identity independently.
- You want to avoid the latency of calling the auth-service on every authenticated request.
- You’re building an API that accepts
Authorization: Bearer <token>headers.
JWKS is included in the Pro plan.
Enable JWKS in bao.config.json
{
"auth": {
"plugins": {
"jwks": {
"enabled": true
}
}
}
}Push the config change and re-run the deploy workflow. The auth-service will begin issuing JWTs on sign-in and expose a public key set at /.well-known/jwks.json.
Client setup
The browser-side client is identical to the session-based setup. No changes needed — the JWT is issued automatically alongside the session cookie.
// lib/auth-client.ts
import { createAuthClient } from "better-auth/client";
export const authClient = createAuthClient({
baseURL: "https://my-app-production.workers.dev",
});Sign in as normal — the auth-service sets both a session cookie and a JWT cookie in the response.
Verify a JWT in your backend
Install jose — a zero-dependency JWT library that works in any JS runtime including Cloudflare Workers.
npm install joseFetch the public keys from your auth-service once and cache them, then verify incoming tokens:
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://my-app-production.workers.dev/.well-known/jwks.json"),
);
export async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, JWKS);
return payload; // contains userId, email, etc.
}createRemoteJWKSet handles key caching and rotation automatically.
Cloudflare Worker example
Read the JWT from the Authorization header or from the cookie set by the auth-service:
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://my-app-production.workers.dev/.well-known/jwks.json"),
);
export default {
async fetch(request: Request): Promise<Response> {
const auth = request.headers.get("Authorization");
const token = auth?.replace("Bearer ", "");
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
try {
const { payload } = await jwtVerify(token, JWKS);
// payload.sub is the user ID
return new Response(`Hello ${payload.sub}`);
} catch {
return new Response("Invalid token", { status: 401 });
}
},
};Next.js example
Verify the token in a Route Handler or middleware without calling the auth-service:
// app/api/protected/route.ts
import { createRemoteJWKSet, jwtVerify } from "jose";
import { cookies } from "next/headers";
const JWKS = createRemoteJWKSet(
new URL(`${process.env.AUTH_SERVICE_URL}/.well-known/jwks.json`),
);
export async function GET() {
const token = (await cookies()).get("better-auth.jwt")?.value;
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const { payload } = await jwtVerify(token, JWKS);
return Response.json({ userId: payload.sub });
} catch {
return Response.json({ error: "Invalid token" }, { status: 401 });
}
}Set AUTH_SERVICE_URL in .env.local to your worker URL.
JWKS endpoint
The public key set is served at:
https://{your-worker-url}/.well-known/jwks.jsonKeys are rotated automatically by your bao auth-service. createRemoteJWKSet from jose handles re-fetching when it encounters a key ID not present in its local cache.