Add Firebase Authentication sign-in to a Better Auth app. better-auth-firebase-auth is a Better Auth plugin for phone (SMS) codes, Google Sign-In and email/password through Firebase. Firebase checks who the user is; Better Auth owns the session, users, organizations and plugins. Setup is one server plugin, one client plugin and one call per sign-in method. It is listed on Better Auth's community plugins page.
- No extra services: Firebase sends the SMS codes (no Twilio) and the password reset emails (no SendGrid or Resend).
- Keep the data in Firestore too: pair it with
better-auth-firestoreso Better Auth needs no second database. This package signs users in; that one stores the data. See Firestore Adapter. - Install:
pnpm add better-auth-firebase-auth better-auth firebase firebase-admin - Docs: Quick start • Phone • Google • Email/password • FAQ
"Add phone sign-in with Firebase to my Better Auth app."
With the agent skill installed, Claude Code, Cursor or Codex follows it: installs the packages, keeps
firebase-adminout of client code, adds both plugins, builds the reCAPTCHA and SMS code flow, and passes the Firebase ID token toauthClient.signInWithPhone({ idToken }).
-
Install the plugin with Better Auth and both Firebase SDKs (pnpm, yarn and bun work the same way):
npm install better-auth-firebase-auth better-auth firebase firebase-admin
-
Turn on a sign-in method in Firebase Console → Authentication → Sign-in method. This example uses Google.
-
Server: initialize Firebase Admin once and add the plugin (
lib/auth.ts). Better Auth still needs its own database andBETTER_AUTH_SECRET, as in its installation guide.import { betterAuth } from "better-auth"; import { firebaseAuthPlugin } from "better-auth-firebase-auth/server"; import { cert, getApps, initializeApp } from "firebase-admin/app"; import { getAuth } from "firebase-admin/auth"; if (getApps().length === 0) { initializeApp({ credential: cert({ projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"), }), }); } export const auth = betterAuth({ plugins: [firebaseAuthPlugin({ firebaseAdminAuth: getAuth() })], });
-
Client: initialize Firebase and add the client plugin (
lib/auth-client.ts). The values come from Firebase Console → Project settings → Your apps.import { createAuthClient } from "better-auth/react"; import { firebaseAuthClientPlugin } from "better-auth-firebase-auth/client"; import { getApps, initializeApp } from "firebase/app"; if (getApps().length === 0) { initializeApp({ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, }); } export const authClient = createAuthClient({ plugins: [firebaseAuthClientPlugin()], });
-
Sign in: Firebase signs the user in, and the plugin turns its ID token into a Better Auth session.
import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth"; import { authClient } from "@/lib/auth-client"; const result = await signInWithPopup(getAuth(), new GoogleAuthProvider()); await authClient.signInWithGoogle({ idToken: await result.user.getIdToken() });
That's the whole setup. Phone and email/password sign-in use the same two plugins; see Firebase Phone Authentication and Email/Password Authentication.
Install the agent skill once, and Claude Code, Cursor, Codex, Copilot, Windsurf and 70+ other agents can set the plugin up from a plain request:
npx skills add yultyyev/better-auth-firebase-authThen paste one of these prompts:
Add Firebase phone (SMS) sign-in to my Better Auth app.
Add Google sign-in through Firebase to my Better Auth app.
Add email/password sign-in and password reset through Firebase to my Better Auth app.
Move my app from Firebase Auth to Better Auth and keep my existing Firebase users.
Should I use Firebase Phone Auth or Better Auth's phoneNumber plugin?
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. How a sign-in links to an existing Better Auth user is covered in Account linking and security.
- 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.
Social providers (Facebook, GitHub, Twitter/X, Microsoft, Apple, LinkedIn), anonymous auth, SAML/OIDC, MFA, and custom tokens. Contributions welcome — see Contributing.
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";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.
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 session
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 setNo 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`
})- 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
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 });import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
const credential = await signInWithEmailAndPassword(
getAuth(),
"user@example.com",
"password",
);
const idToken = await credential.user.getIdToken();
await authClient.signInWithEmail({ idToken });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: "user@example.com",
password: "password",
});Route Better Auth's built-in /sign-in/email and /sign-up/email endpoints through Firebase:
firebaseAuthPlugin({
overrideEmailPasswordFlow: true,
firebaseConfig: { ... },
firebaseAdminAuth: getAuth(),
})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.
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",
})// 1. Send reset email
await authClient.sendPasswordReset({ email: "user@example.com" });
// 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" });When serverSideOnly: true, no endpoints are registered. Auth runs entirely through hooks:
firebaseAuthPlugin({
serverSideOnly: true,
overrideEmailPasswordFlow: true,
firebaseConfig: { ... },
firebaseAdminAuth: getAuth(),
})| 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+). |
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.
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.
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 |
Starting a new app? Use Better Auth 1.7.3 or later. Nothing below applies to you.
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.
-
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.
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 paused
Pass
--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 writing
Or 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).
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.
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.
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.
Yes. better-auth-firebase-auth is a Better Auth plugin for Firebase Auth, listed on Better Auth's community plugins page. 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.
Keep Firebase Authentication for sign-in and add this plugin. Existing Firebase users sign in as before; on each user's first sign-in through the plugin, Better Auth creates their user and links it to their Firebase UID. There are no password hashes to export and no import step. An existing Better Auth user with the same email is linked only when both emails are verified.
Install the agent skill with npx skills add yultyyev/better-auth-firebase-auth, then ask in plain words, for example "Add phone sign-in with Firebase to my Better Auth app." The skill covers phone, Google and email/password sign-in, password reset, the Firestore adapter, and choosing between Firebase Phone Auth and Better Auth's phoneNumber plugin. More prompts are in Set it up with your AI agent.
Yes. Pair this plugin with better-auth-firestore, which stores Better Auth's users, sessions and accounts in your project's Firestore. This plugin signs users in through Firebase Authentication; the adapter stores the data. See Firestore Adapter.
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.
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.
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.
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.
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.
No. overrideEmailPasswordFlow defaults to false. Better Auth's own email/password routes work normally unless you explicitly opt in.
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.
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.
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({ ... })],
});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-authSkill page: skills.sh/yultyyev/better-auth-firebase-auth/firebase-auth-better-auth
Contributions are welcome. Please follow the Better Auth Contributing Guide for development setup and code style.
MIT