Firestore Adapter for Better Auth
better-auth-firestore is the Firestore database adapter for Better Auth. It stores users, sessions, accounts, and verification tokens in Firestore using the Firebase Admin SDK.
Package: better-auth-firestore — GitHub · npm
Install
pnpm add better-auth-firestore firebase-admin better-auth
Minimal setup
import { firestoreAdapter } from "better-auth-firestore";
import { betterAuth } from "better-auth";
import { getFirestore } from "firebase-admin/firestore";
export const auth = betterAuth({
database: firestoreAdapter({ firestore: getFirestore() }),
});
Full setup with credentials
import { betterAuth } from "better-auth";
import { firestoreAdapter, initFirestore } from "better-auth-firestore";
import { cert } from "firebase-admin/app";
const firestore = initFirestore({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID!,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
privateKey: process.env.FIREBASE_PRIVATE_KEY!.replace(/\\n/g, "\n"),
}),
projectId: process.env.FIREBASE_PROJECT_ID!,
name: "better-auth",
});
export const auth = betterAuth({
database: firestoreAdapter({
firestore,
namingStrategy: "default", // or "snake_case"
collections: {
// users: "users",
// sessions: "sessions",
// accounts: "accounts",
// verificationTokens: "verificationTokens",
},
}),
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
firestore |
Firestore |
getFirestore() |
Firebase Admin Firestore instance |
namingStrategy |
"default" | "snake_case" |
"default" |
Collection naming convention |
collections |
object |
see below | Override individual collection names |
debugLogs |
boolean |
false |
Enable verbose query logging |
Default collection names:
users→"users"sessions→"sessions"accounts→"accounts"verificationTokens→"verificationTokens"(default) or"verification_tokens"(snake_case)
Firestore composite index — not required (v1.1+)
No composite index is required. The adapter never combines a where filter with a Firestore orderBy: it applies the filter server-side and sorts the results in memory. Verification-token lookups (identifier == ordered by createdAt desc) work with Firestore's automatic single-field indexes alone. As of v1.3 the same holds for rateLimit.storage: "database": the native incrementOne sends only equality filters to Firestore and checks the limiter's range guards in memory inside a transaction.
If sign-in fails with 9 FAILED_PRECONDITION: The query requires an index (Better Auth may surface this as Failed to parse state), you are on an older version. Upgrade to v1.1 or later — do not create the index. Any composite index created for a previous version can be removed afterward.
Optional tooling (only for advanced setups that query the verification collection directly, outside the adapter): generateIndexSetupUrl(projectId, databaseId?, collectionName?) and getIndexConfig(collectionName?), plus the bundled firestore.indexes.json. These default to the verificationTokens collection (use verification_tokens for the snake_case strategy).
Upgrading to Better Auth 1.7
Better Auth 1.7 needs adapter v1.3+ (it made incrementOne a required adapter method; older adapters throw Adapter "firestore" must implement incrementOne for atomic guarded counter updates on rate limiting, organization invitations, device authorization, and two-factor). v1.3 also works with Better Auth 1.6, so upgrade the adapter first.
1.7 identifies accounts by (issuer, accountId) and stores issuer on every account. Existing Firestore documents don't have it, and Firestore has no npx auth migrate — existing users cannot sign in after upgrading until the field is backfilled:
# Same credentials as the app (GOOGLE_APPLICATION_CREDENTIALS, FIREBASE_* vars, or --service-account key.json)
npx better-auth-firestore backfill-account-issuers # dry run — review the report
npx better-auth-firestore backfill-account-issuers --apply # write, with auth writes paused
# add --collection / --naming-strategy snake_case to match the adapter config.
# google/apple/facebook/line resolve to their real issuer automatically; pass
# --issuer <providerId>=<url> for providers whose issuer depends on config or
# the token — cognito, paybin, microsoft (Entra ID), okta, auth0, keycloak —
# e.g. --issuer okta=https://acme.okta.com. They are reported unresolved
# (exit 1) until you do, rather than stamped with a value that may be wrong.
Ran the backfill on adapter v1.3.0? It stamped
local:oauth:google(and the same forapple/line) instead of the real issuer, so those users still can't sign in. Upgrade and re-run the command — it detects and repairs those documents, reporting them as "wrong issuers left by the v1.3.0 backfill". The startup warning cannot catch this: the field is present, just wrong.
Programmatic equivalent: backfillAccountIssuers({ firestore, dryRun, issuers, resolveIssuer }). The adapter also warns once at startup ([better-auth-firestore] … Run: npx better-auth-firestore backfill-account-issuers …) when account documents lack issuer; migrationChecks: false disables the check.
Run the backfill before the first deploy on Better Auth 1.7 (the helper ships in v1.3 and is harmless on 1.6). Full details: https://better-auth.com/docs/guides/1-7-upgrade-guide
Environment variables
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
Important: FIREBASE_PRIVATE_KEY often arrives with literal \n strings in env vars. Always replace them:
privateKey: process.env.FIREBASE_PRIVATE_KEY!.replace(/\\n/g, "\n")
Migration from Auth.js / NextAuth Firebase adapter
better-auth-firestore uses the same collection names and field shapes as the Auth.js Firebase adapter by default — it is a drop-in replacement.
// Before (Auth.js)
import { FirestoreAdapter } from "@auth/firebase-adapter";
// After (Better Auth)
import { firestoreAdapter } from "better-auth-firestore";
export const auth = betterAuth({
database: firestoreAdapter({ firestore }),
});
No Firestore data migration needed. Same users, sessions, accounts, and verificationTokens collections.
Using with the Firebase Auth plugin
To also use Firebase Authentication (Phone OTP, Google Sign-In, Email/Password), combine with better-auth-firebase-auth:
import { firestoreAdapter } from "better-auth-firestore";
import { firebaseAuthPlugin } from "better-auth-firebase-auth/server";
export const auth = betterAuth({
database: firestoreAdapter({ firestore }),
plugins: [firebaseAuthPlugin({ firebaseAdminAuth: getAuth() })],
});
Firestore Emulator (local development & tests)
# Start emulator
docker run -d --rm -p 8080:8080 google/cloud-sdk:emulators \
gcloud beta emulators firestore start --host-port=0.0.0.0:8080
# Set env and start dev server
FIRESTORE_EMULATOR_HOST=localhost:8080 pnpm dev
# Run tests
FIRESTORE_EMULATOR_HOST=localhost:8080 pnpm vitest run
No credentials or service account needed when using the emulator.
Runtime support
| Runtime | Supported |
|---|---|
| Node 22+ | ✅ Required (LTS+). ESM build; require() needs Node 22.12+ |
| Next.js on Vercel (Node.js runtime) | ✅ Recommended |
| Cloud Functions / Cloud Run | ✅ |
Vercel Edge Runtime (runtime = 'edge') |
❌ Admin SDK requires Node.js |
| Cloudflare Workers | ❌ Admin SDK requires Node.js |
Note: Vercel deploys work fine — the restriction is only when you explicitly opt into the Edge Runtime (export const runtime = 'edge'). The default Node.js serverless runtime on Vercel is fully supported.
Common mistakes
The query requires an indexon verification tokens — You're on a version older than v1.1. Upgradebetter-auth-firestore; the adapter now sorts filtered queries in memory and needs no composite index.The query requires an indexonrateLimit— You're on a version older than v1.3. Upgrade; the nativeincrementOneneeds no composite index. Do not create the index.Adapter "firestore" must implement incrementOne— Better Auth 1.7 with an adapter older than v1.3. Upgradebetter-auth-firestore.- Existing users can't sign in after moving to Better Auth 1.7 — The
account.issuerbackfill was not run (the server log shows a[better-auth-firestore]warning with the command). Runnpx better-auth-firestore backfill-account-issuers --apply(see above). - Only the social users can't sign in, and the backfill reports nothing to do — You ran the backfill on adapter v1.3.0, which stamped
local:oauth:googleinstead ofhttps://accounts.google.com(same forapple/facebook/line). The field is present but wrong, so the startup warning stays silent. Upgrade the adapter and re-runbackfill-account-issuers --apply; it repairs those documents. - FIREBASE_PRIVATE_KEY with literal
\n— Always call.replace(/\\n/g, "\n")on the key before passing tocert(). - Using at edge runtime — Firebase Admin SDK does not run on Vercel Edge or Cloudflare Workers. Use Node.js runtimes only.
npm error 404on@yultyyev/better-auth-firestore— The scoped package was unpublished from npm (2026-08). Switch the dependency and import path tobetter-auth-firestore; the API is identical.
Reporting issues
If behavior still looks like a library bug after checking the mistakes above:
- First confirm the project is on the latest version — many reports (e.g. the composite-index error) are already fixed.
- Only file an issue when the user explicitly asks — never open one autonomously.
- Redact secrets and PII before filing: Firebase project IDs,
FIREBASE_PRIVATE_KEY, tokens, and anycreate_compositeindex URL (it encodes the project path). Include the package version and a minimal repro instead.
Issues: https://github.com/yultyyev/better-auth-firestore/issues