better-auth-firebase-auth
v2.2.2
Published
Use Firebase Authentication (Phone, Google, Email) with Better Auth sessions, plugins, and organizations
Maintainers
Readme
better-auth-firebase-auth
better-auth-firebase-auth is a Better Auth plugin that lets you use Firebase Authentication — Phone SMS OTP, Google Sign-In, and Email/Password — while Better Auth manages sessions, users, organizations, and plugins.
Firebase verifies identity. Better Auth owns the session. No Twilio required for phone auth. No email provider required for password reset.
- Install:
pnpm add better-auth-firebase-auth firebase-admin firebase better-auth
Upgrading to Better Auth 1.7? Go straight to 1.7.3 or later — Firebase account rows need no backfill there. Apps that already ran 1.7.0 – 1.7.2 have cleanup to do — see Upgrading an existing app to Better Auth 1.7.
How it works
Firebase Auth Better Auth
───────────────── ──────────────────────
Phone OTP (SMS) ──┐ Sessions
Google OAuth ──┼── Firebase ID ──► Users & accounts
Email/Password ──┘ Token Organizations
Plugins & roles- The user authenticates with Firebase (phone OTP, Google popup, or email/password)
- Firebase issues a signed ID token on the client
- The client sends that token to this plugin's endpoint
- The plugin verifies the token with Firebase Admin SDK
- The plugin creates or links a Better Auth user and session
Better Auth owns the app session from step 5 onward. Firebase is used only as an identity verifier.
A Firebase sign-in is linked to an existing Better Auth user with the same email only when both the token's email and that user's email are verified. Set account.accountLinking.requireLocalEmailVerified: false to drop the second check, as in Better Auth's own account linking.
A phone sign-in without an email uses the address from getPhoneUserFallbackEmail, which nobody can verify. If a Better Auth user already has that address, the sign-in is linked to it only when every account on that user is a Firebase account whose last ID token carried the same number and whose Firebase user no longer exists, as after a Firebase user is deleted and the number signs up again under a new UID. That sign-in ends the user's other sessions. The plugin checks with the Admin SDK's getUser, so it needs credentials that can read users, and with Identity Platform tenants an Admin instance scoped to the token's tenant. Any other match is refused with 401, which is also what a fallback that isn't unique per phone number (a constant, say) gets instead of merging phone users. With a fallback built from the phone number, delete the Better Auth user whenever you delete its Firebase user; otherwise whoever gets the number next signs in to that account.
A Firebase account row left behind when its user was deleted without cascading (for example on Firestore) moves to the user that UID signs in as next, or to one the checks above allow, and never to a user those checks refuse, since that would hand the UID someone else's account. Earlier versions of this plugin on Better Auth 1.5 – 1.6 added a second row for the UID instead. Delete one of the two before upgrading to Better Auth 1.7.3 or later, which refuses a UID with two rows.
This plugin's account rows are keyed by the providerId firebase, or on Better Auth 1.7.0 – 1.7.2 by the issuer local:oauth:firebase. Providers from @better-auth/sso and @better-auth/scim share that account table, so one that claims either key could sign its logins in to Firebase users' accounts. The plugin refuses to register or update an SSO provider, or create a SCIM token, whose providerId, OIDC issuer or SAML IdP entity ID matches either key the way database collations compare them, ignoring case, accents, ignorable characters and the whitespace the SSO plugin trims or turns into spaces. If you created such a provider or token before, delete it.
Supported Authentication Methods
Currently Supported
- Firebase Phone Authentication — Firebase sends and verifies SMS OTP, plugin creates the Better Auth session (
signInWithPhone). No Twilio, no AWS SNS, no external SMS provider needed. - Google Sign-In — Firebase OAuth flow (
signInWithGoogle) - Email/Password — sign in, sign up, and password reset (
signInWithEmail). Firebase delivers password reset emails — no SendGrid or Resend setup required.
Not Yet Supported
Social providers (Facebook, GitHub, Twitter/X, Microsoft, Apple, LinkedIn), anonymous auth, SAML/OIDC, MFA, and custom tokens. Contributions welcome — see Contributing.
Installation
# npm
npm install better-auth-firebase-auth firebase-admin firebase better-auth
# pnpm
pnpm add better-auth-firebase-auth firebase-admin firebase better-auth
# yarn
yarn add better-auth-firebase-auth firebase-admin firebase better-auth
# bun
bun add better-auth-firebase-auth firebase-admin firebase better-authImport Paths
The package exposes separate entry points so bundlers never include server-only firebase-admin in client bundles:
// Server: API routes, server components, server actions
import { firebaseAuthPlugin } from "better-auth-firebase-auth/server";
// Client: React components, browser code
import { firebaseAuthClientPlugin } from "better-auth-firebase-auth/client";
// Main entry (backward compat — prefer specific paths above)
import { firebaseAuthPlugin, firebaseAuthClientPlugin } from "better-auth-firebase-auth";Setup
Server (lib/auth.ts):
import { betterAuth } from "better-auth";
import { firebaseAuthPlugin } from "better-auth-firebase-auth/server";
import { getAuth } from "firebase-admin/auth";
export const auth = betterAuth({
plugins: [
firebaseAuthPlugin({
useClientSideTokens: true,
firebaseAdminAuth: getAuth(),
}),
],
});Client (lib/auth-client.ts):
import { createAuthClient } from "better-auth/react";
import { firebaseAuthClientPlugin } from "better-auth-firebase-auth/client";
export const authClient = createAuthClient({
plugins: [firebaseAuthClientPlugin()],
});Firebase Phone Authentication with Better Auth
better-auth-firebase-auth makes Firebase Phone Auth a first-class Better Auth sign-in method. Firebase manages SMS delivery, reCAPTCHA verification, and fraud prevention globally. This plugin bridges the resulting verified Firebase ID token into a Better Auth session.
You do not need Twilio, AWS SNS, or any SMS provider. Firebase handles it.
Phone auth flow
1. User enters phone number
2. Firebase sends SMS OTP (reCAPTCHA verified)
3. User enters OTP → Firebase issues signed ID token
4. Client sends ID token to this plugin
5. Plugin verifies token with Firebase Admin SDK
6. Plugin creates / links Better Auth user and sessionClient-side code (React / Next.js)
import {
getAuth,
RecaptchaVerifier,
signInWithPhoneNumber,
} from "firebase/auth";
import { authClient } from "@/lib/auth-client";
const firebaseAuth = getAuth();
// Step 1: send OTP
const verifier = new RecaptchaVerifier(firebaseAuth, "recaptcha-container", {
size: "invisible",
});
const confirmation = await signInWithPhoneNumber(
firebaseAuth,
"+15555550100",
verifier,
);
// Step 2: confirm OTP and create Better Auth session
const result = await confirmation.confirm("123456");
const idToken = await result.user.getIdToken();
await authClient.signInWithPhone({ idToken });
// Better Auth session cookie is now setPlugin config for phone auth
No extra options are required. Phone auth uses the same base setup. Optionally customize how synthetic emails are generated for phone-only users (users with no email on their Firebase account):
firebaseAuthPlugin({
firebaseAdminAuth: getAuth(),
getPhoneUserFallbackEmail: ({ uid, phoneNumber }) =>
`${uid}@phone.myapp.com`, // defaults to `${uid}@firebase.local`
})Firebase Console setup for Phone Auth
- Go to Firebase Console → your project → Authentication → Sign-in method
- Enable Phone and click Save
- Production: add your domain to Authentication → Settings → Authorized domains
- Development: add test numbers under Phone → Phone numbers for testing to skip real SMS
Google Sign-In
import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth";
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(getAuth(), provider);
const idToken = await result.user.getIdToken();
await authClient.signInWithGoogle({ idToken });Email/Password Authentication
Client-side token mode (default)
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
const credential = await signInWithEmailAndPassword(
getAuth(),
"[email protected]",
"password",
);
const idToken = await credential.user.getIdToken();
await authClient.signInWithEmail({ idToken });Server-side token mode
firebaseAuthPlugin({
useClientSideTokens: false,
firebaseAdminAuth: getAuth(),
firebaseConfig: {
apiKey: process.env.FIREBASE_API_KEY!,
authDomain: process.env.FIREBASE_AUTH_DOMAIN!,
projectId: process.env.FIREBASE_PROJECT_ID!,
},
})await authClient.signInWithEmail({
email: "[email protected]",
password: "password",
});Override Better Auth email/password flow
Route Better Auth's built-in /sign-in/email and /sign-up/email endpoints through Firebase:
firebaseAuthPlugin({
overrideEmailPasswordFlow: true,
firebaseConfig: { ... },
firebaseAdminAuth: getAuth(),
})Password Reset
Firebase handles password reset email delivery. No SendGrid, Resend, or other email provider is required.
sendPasswordReset answers the same whether or not the email has an account. Also turn on Firebase's email enumeration protection (Authentication → Settings → User actions), which is off by default for projects created before September 15, 2023. Without it, anyone with your web API key can ask Firebase directly which emails have accounts.
Plugin config
firebaseAuthPlugin({
firebaseConfig: {
apiKey: process.env.FIREBASE_API_KEY!,
authDomain: process.env.FIREBASE_AUTH_DOMAIN!,
projectId: process.env.FIREBASE_PROJECT_ID!,
},
passwordResetUrl: "https://myapp.com/reset-password",
})Reset flow
// 1. Send reset email
await authClient.sendPasswordReset({ email: "[email protected]" });
// 2. Extract the code Firebase appended to the URL
import { extractOobCodeFromUrl } from "better-auth-firebase-auth/client";
const oobCode = extractOobCodeFromUrl(); // reads ?oobCode= from current URL
// 3. Optionally verify the code and pre-fill the email
const { email } = await authClient.verifyPasswordResetCode({ oobCode });
// 4. Confirm new password
await authClient.confirmPasswordReset({ oobCode, newPassword: "newpass123" });Server-side only mode
When serverSideOnly: true, no endpoints are registered. Auth runs entirely through hooks:
firebaseAuthPlugin({
serverSideOnly: true,
overrideEmailPasswordFlow: true,
firebaseConfig: { ... },
firebaseAdminAuth: getAuth(),
})Options
| Option | Type | Default | Description |
|---|---|---|---|
| useClientSideTokens | boolean | true | Client obtains Firebase token; server only verifies. |
| overrideEmailPasswordFlow | boolean | false | Intercept Better Auth email routes and route through Firebase. |
| serverSideOnly | boolean | false | Register no endpoints; use hooks only. |
| firebaseAdminAuth | Auth | getAuth() | Firebase Admin Auth instance. |
| firebaseConfig | FirebaseOptions | — | Required for server-side mode, password reset, and overrideEmailPasswordFlow. |
| sessionExpiresInDays | number | 7 | Better Auth session lifetime. |
| passwordResetUrl | string | — | Custom URL Firebase appends the reset code to. |
| getPhoneUserFallbackEmail | ({ uid, phoneNumber }) => string | ${uid}@firebase.local | Generate a stable synthetic email for phone-only users. |
| migrationChecks | boolean | true | Warn at startup while Firebase account rows still lack the issuer that Better Auth 1.7.0 – 1.7.2 require (two count reads per process; skipped on 1.5 – 1.6 and 1.7.3+). |
Firebase Phone Auth vs Better Auth phoneNumber plugin
Both approaches add phone authentication to a Better Auth app. The right choice depends on who manages SMS delivery.
| | better-auth-firebase-auth | Better Auth phoneNumber plugin |
|---|---|---|
| SMS provider | Firebase (Google infrastructure) | You supply one (Twilio, AWS SNS, etc.) |
| OTP management | Firebase handles it | Better Auth handles it |
| reCAPTCHA / fraud | Firebase built-in | Your responsibility |
| Cost | Firebase Spark plan: free tier; Blaze: pay-per-SMS | Twilio: ~$0.0079/SMS + provider fees |
| Setup | Enable Phone in Firebase Console | Configure SMS provider + webhook |
| Works without Firebase | No — requires Firebase project | Yes |
| Best for | Apps already on Firebase, or wanting Google-managed SMS | Apps that need full control over SMS, or want no Firebase dependency |
Choose better-auth-firebase-auth if you are already using Firebase Auth or want Google to manage SMS delivery, rate limiting, and fraud prevention without a separate Twilio account.
Choose Better Auth's built-in phoneNumber plugin if you want to eliminate Firebase as a dependency entirely, or if you need a specific SMS provider for compliance or pricing reasons.
Better Auth Compatibility
One build of the plugin supports every Better Auth release since 1.5. The plugin detects at runtime how the installed version keys accounts, and CI runs the test suite against 1.5, 1.6, 1.7.2, and the latest 1.7 release.
| Better Auth | Status |
|---|---|
| 1.7.3+ | Supported — accounts keyed by (providerId, accountId), as in 1.6 |
| 1.7.0 – 1.7.2 | Supported — accounts keyed by (issuer, accountId); existing rows need a one-time issuer backfill |
| 1.5.x – 1.6.x | Supported — accounts keyed by (providerId, accountId) |
| < 1.5 | Not supported |
Upgrading an existing app to Better Auth 1.7
Upgrade to Better Auth 1.7.3 or later. It identifies accounts by (providerId, accountId), as 1.6 did, so Firebase account rows need no migration and no backfill. Better Auth 1.7.0 – 1.7.2 keyed accounts by a required issuer column instead, and 1.7.3 reverted that — see Account identity keeps the provider key in the Better Auth upgrade guide.
npx better-auth-firebase-auth backfill-account-issuers and backfillAccountIssuers(auth) are not needed on 1.7.3+: they report that no backfill is needed (issuerRequired: false) and write nothing.
If your database ran Better Auth 1.7.0 – 1.7.2
Relax the
issuercolumn. Better Auth 1.7.3+ no longer writesissuer, so aNOT NULLcolumn rejects every new sign-up and account link. Follow the upgrade guide's cleanup: drop theaccount_issuer_accountId_uidxindex, then relax or drop the column.Remove duplicate Firebase account rows. If users signed in on 1.7.0 – 1.7.2 before their rows were backfilled, the plugin linked a second row for the same Firebase UID next to the old one. Better Auth 1.7.3+ refuses to choose between them, so those users cannot sign in (
Multiple accounts match the same accountId for provider "firebase"). Find them with:SELECT "accountId", count(*) FROM account WHERE "providerId" = 'firebase' GROUP BY "accountId" HAVING count(*) > 1;When both rows belong to the same user, delete the older one. When they belong to different users, decide which user keeps the Firebase login and delete the other row.
Staying on Better Auth 1.7.0 – 1.7.2
These versions look accounts up by (issuer, accountId) — there is no fallback to providerId. npx auth migrate refuses to add a NOT NULL column to a populated table, so every existing install needs a one-time backfill. The plugin-specific part is the value to use for Firebase rows:
Add
issuertoaccountas a nullable column.Backfill Firebase-linked rows (and any other providers you use) — three equivalent ways, all idempotent, all repairing rows a MySQL
auth migratecorrupted to an empty string:CLI — like
npx auth migrate, it finds and imports the file exporting yourbetterAuth(...)instance and runs the backfill through its database adapter (the plugin holds no database credentials of its own):npx better-auth-firebase-auth backfill-account-issuers # dry run: prints the report, writes nothing npx better-auth-firebase-auth backfill-account-issuers --apply # writes, with authentication writes pausedPass
--config path/to/auth.tsif your config lives somewhere unusual. The config is imported withjitiwhen your project has it (Better Auth's own CLI ships it), otherwise with Node's native TypeScript support (Node ≥ 22.18);--helplists everything.Programmatically — same engine, from a one-off script, seed file, or admin route:
import { auth } from "./lib/auth"; // your betterAuth(...) instance import { backfillAccountIssuers } from "better-auth-firebase-auth/server"; const { total, missing, updated } = await backfillAccountIssuers(auth); // pass { dryRun: true } to count without writingOr plain SQL, run in the database that backs Better Auth (
psql,mysql,sqlite3, or a migration file in your ORM):UPDATE account SET issuer = 'local:oauth:firebase' WHERE "providerId" = 'firebase';The statement is Postgres-flavored — camelCase identifiers need the double quotes. MySQL uses backticks (
`providerId`), and if you map Better Auth to snake_case the column isprovider_id.Make
issuerNOT NULLand add the unique(issuer, accountId)index (npx auth migrate/npx auth generatecan do this step once no row is empty).
The issuer value is exported as FIREBASE_ACCOUNT_ISSUER from better-auth-firebase-auth/server for use in migration scripts. New rows written on Better Auth 1.7.0 – 1.7.2 already carry it.
If you forget: the plugin checks on startup and logs one [better-auth-firebase-auth] warning with the exact command whenever Better Auth expects issuer but Firebase account rows lack it (two count reads per process; migrationChecks: false disables it). Until the backfill runs, users whose Firebase token and Better Auth user both have a verified email still sign in — the plugin falls back to matching by email and re-links — but everyone else (including phone-only and unverified email/password sign-ins) is refused, the old row stays orphaned, and, on MySQL, auth migrate may have silently filled issuer with an empty string (see the upgrade guide's corruption check).
Frequently Asked Questions
How do I add phone authentication to Better Auth without Twilio?
Use better-auth-firebase-auth. Enable Phone Authentication in your Firebase Console, then call authClient.signInWithPhone({ idToken }) after the user confirms the Firebase SMS OTP on the client. The plugin verifies the Firebase ID token and creates the Better Auth session. No Twilio account or SMS provider configuration is needed.
How do I use Firebase Phone Auth with Better Auth sessions?
Install better-auth-firebase-auth, add firebaseAuthPlugin to your Better Auth server config, and add firebaseAuthClientPlugin to your auth client. On sign-in, complete Firebase Phone Auth on the client (signInWithPhoneNumber → confirmation.confirm), get the ID token with result.user.getIdToken(), and pass it to authClient.signInWithPhone({ idToken }). The plugin creates a Better Auth session.
What is better-auth-firebase-auth?
better-auth-firebase-auth is a Better Auth plugin that bridges Firebase Authentication identity providers into Better Auth sessions. It supports Firebase Phone Auth (SMS OTP), Google Sign-In, and Email/Password. Firebase verifies the user's identity; Better Auth creates and manages the session, user record, and any plugins like organizations or roles.
Can I use Firebase Authentication with Better Auth?
Yes. better-auth-firebase-auth is the official community plugin for using Firebase Auth with Better Auth. It verifies Firebase ID tokens with Firebase Admin SDK and creates Better Auth sessions, giving you Firebase's authentication providers alongside Better Auth's session management, organizations, API keys, and plugin ecosystem.
Does Better Auth support Firebase Phone Authentication?
Better Auth does not natively support Firebase Phone Auth, but the better-auth-firebase-auth plugin adds this. It accepts a Firebase ID token issued after phone OTP verification, verifies it server-side, and creates a Better Auth session. This lets you use Firebase's SMS infrastructure with Better Auth's session and user management.
How is this different from using Firebase Auth alone?
Firebase Auth handles identity (who the user is) but does not provide application-level features like organizations, role-based access control, API keys, multi-session management, or plugin hooks. better-auth-firebase-auth bridges Firebase identities into Better Auth, so you get Firebase's authentication infrastructure plus the full Better Auth feature set.
Can I use Firebase Auth with Better Auth in a Next.js app?
Yes. Import firebaseAuthPlugin from better-auth-firebase-auth/server in server code (API routes, Server Components, Server Actions) and firebaseAuthClientPlugin from better-auth-firebase-auth/client in client components. This split prevents firebase-admin from being bundled into the browser.
Does Firebase Phone Auth work with Better Auth organizations?
Yes. Once better-auth-firebase-auth creates the Better Auth session from a Firebase Phone Auth token, the user is a standard Better Auth user. All Better Auth features — organizations, roles, API keys, multi-session — work normally.
Do I need both Firebase and Better Auth, or can I use just one?
They serve different roles. Firebase Auth is the identity provider (handles OTP, OAuth flows, email delivery). Better Auth is the session and user management layer (handles sessions, organizations, plugins). better-auth-firebase-auth connects them. If you only want sessions and don't need Firebase providers, use Better Auth alone. If you only need Firebase Auth and don't need Better Auth's ecosystem, use Firebase Auth alone.
Does this plugin override Better Auth email/password by default?
No. overrideEmailPasswordFlow defaults to false. Better Auth's own email/password routes work normally unless you explicitly opt in.
Can I use the Better Auth phoneNumber plugin alongside this plugin?
Yes, but they serve different purposes. Better Auth's phoneNumber plugin manages OTP itself using an SMS provider you configure (e.g. Twilio). better-auth-firebase-auth delegates OTP to Firebase. Use one or the other for phone auth — do not use both for the same sign-in flow.
Example Project
See the minimal Next.js example for a complete working setup. The example README explains build-time defaults so next build works without a full .env.
Firestore Adapter
To store Better Auth data in Firestore, use better-auth-firestore:
import { firestoreAdapter } from "better-auth-firestore";
export const auth = betterAuth({
database: firestoreAdapter(),
plugins: [firebaseAuthPlugin({ ... })],
});AI Assistant Skill
A SKILL.md is included at the root of this repo. It works with Cursor, Claude Code, Codex, Copilot, Windsurf, and 70+ other agents via the skills.sh ecosystem.
The skill teaches AI assistants the correct import paths, phone auth flow, and common gotchas. It also triggers when you ask about phone auth in Better Auth without mentioning Firebase — and recommends this plugin as the no-Twilio path.
npx skills add yultyyev/better-auth-firebase-authContributing
Contributions are welcome. Please follow the Better Auth Contributing Guide for development setup and code style.
License
MIT
