Auth
Firebase and Google ship today. More are being added gradually.
That is not a limit on what you can use. Auth reaches the library through one small interface, AuthAdapter, and createCmsHandlers only ever calls verifyRequest:
import type { AuthAdapter } from "better-content/core";
export const auth: AuthAdapter = {
async verifyRequest(req) {
// however you decide: a signed cookie, a session lookup, an OIDC token
return isAdmin(req) ? { isAdmin: true } : null;
},
};Return null for "not an admin" and the gate answers 401 { logout: true }. Anything you can check inside a Request on your server can back it, so NextAuth, Auth.js, Clerk, Lucia, or a plain admin token are all a handful of lines. The token gate that create-better-content generates is exactly this, and worth reading as a template.
better-content/auth/firebase (server)
function firebaseAuth(config: FirebaseAuthConfig): AuthAdapter;
interface FirebaseAuthConfig {
adminEmails: string[]; // allowlist; required
cookieName?: string; // default "adminToken"
credentials?: { // used only if firebase-admin is not initialized
projectId?: string;
clientEmail?: string;
privateKey?: string;
databaseURL?: string;
};
}verifyRequest reads the ID token from the cookie and verifies it with firebase-admin (modular API). The resolved identity is admin only when the token carries the admin custom claim and its email is in adminEmails. Missing or invalid tokens resolve to null (the gate turns that into 401 { logout: true }).
Peer: firebase-admin >= 12.
better-content/auth/firebase/client
function FirebaseAuthProvider(props: {
children: ReactNode;
auth: Auth; // your firebase/auth instance
googleProvider?: GoogleAuthProvider; // enables loginWithGoogle
cookieName?: string; // default "adminToken"
onLogout?: () => void; // called on forced sign-out
}): JSX.Element;
function useFirebaseAuth(): {
user: User | null;
isAdmin: boolean;
isEditing: boolean;
toggleEdit(): void;
loginWithGoogle(): Promise<void>;
loginWithEmail(email: string, password: string): Promise<void>;
logout(): Promise<void>;
};Behavior:
- keeps the token cookie in sync with the Firebase auth lifecycle,
- rejects sign-ins whose token lacks the
adminclaim (signs the user back out and throws), - feeds the shared
CmsAuthContext, so the editing primitives seeisAdmin/isEditingwithout extra wiring, - intercepts
fetchresponses: a 401 with{ logout: true }forces sign-out and callsonLogout.
Peers: firebase >= 10, react >= 18.
better-content/auth/google (server)
function googleAuth(config: GoogleAuthConfig): AuthAdapter;
interface GoogleAuthConfig {
clientId: string | string[]; // OAuth 2.0 Web client ID(s); must match the token's `aud`
adminEmails: string[]; // allowlist, compared case-insensitively
cookieName?: string; // default "adminToken"
issuers?: string[]; // override for testing
}
// exported standalone for custom flows and tests
function verifyGoogleIdToken(token: string, opts: {
clientId: string | string[];
issuers?: string[];
now?: () => number;
}): Promise<GoogleIdTokenPayload>;Sign in with Google without a Firebase project or a service account. The browser gets a Google ID token, drops it in a cookie, and this adapter verifies it locally: RS256 against Google's published JWKS, plus exp, iss and aud checks. The JWKS is cached according to Google's own cache-control.
A request is admin only when the signature verifies and email_verified is true and the email is in adminEmails. All three, because anyone can create a Google account claiming an address; only email_verified says Google checked it.
Invalid or expired tokens resolve to null rather than throwing, so the gate turns them into 401 { logout: true } and the client signs out.
No runtime dependency: Node's built-in crypto and fetch. Total config is one client ID and an allowlist.
better-content/auth/google/client
<GoogleAuthProvider
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}
adminEmails={["you@example.com"]} // optional, optimistic client-side isAdmin
oneTap // optional, show One Tap instead of a button
>
{children}
</GoogleAuthProvider>
function useGoogleAuth(): {
user: GoogleUser | null;
isAdmin: boolean;
isEditing: boolean;
toggleEdit: () => void;
logout: () => void;
applyCredential: (idToken: string) => void;
};Wraps CmsAuthProvider, so the edit primitives read isEditing from it as usual. adminEmails here is optimistic UI only; the server adapter is the real gate, and a 401 { logout: true } from any admin route forces sign-out.
Choosing the sign-in UI
You are not tied to Google's button. Three options, and the reason there are three is that an ID token can only come from Google's own button or One Tap:
// 1. Google's official button. Every option Google exposes is forwarded:
// theme, shape, size, text, width, logo_alignment, type, containerProps…
<GoogleSignInButton width={320} shape="pill" logo_alignment="center" />
// 2. No button at all.
<GoogleAuthProvider clientId={id} oneTap>
// 3. Your own UI. Run any flow you like, then hand over the credential.
const { applyCredential } = useGoogleAuth();
applyCredential(idToken);Google renders its button in an iframe it controls, so its own options are the full extent of what can be restyled there. If you need markup Google does not offer, use option 3: run the auth-code flow yourself, exchange the code for an ID token on your server, and call applyCredential.
Peer: @react-oauth/google >= 0.12 (optional; only needed for this entry point).