How to add Firebase user authentication to a Webflow membership site

How to add Firebase user authentication to a Webflow membership site

Learn how to build a Firebase-authenticated members area on Webflow Cloud with Next.js.

How to add Firebase user authentication to a Webflow membership site

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

A Firebase sign-in verified on Webflow Cloud's Workers runtime turns a public Webflow page into a gated members area; the check that matters happens where the browser hands off to the server.

Firebase Authentication gives you a signed-in user in the browser after you configure a provider. Webflow removed its native User Accounts feature on 29 January 2026, so the platform has no first-party authentication left, and identity has to come from somewhere else.

A members area requires the server to refuse to render a page for any request that lacks a verified token. That refusal has to happen somewhere Firebase's client SDK cannot reach.

On a Webflow site, that server is a Next.js app mounted through Webflow Cloud. It runs on Cloudflare Workers. This build stores a server-verified ID token in an httpOnly cookie from a Route Handler and lets middleware.ts redirect anyone without a valid cookie to the login page.

What do you need to add Firebase authentication in Webflow?

You need four prerequisites. Webflow Cloud is available from the free Starter site plan up, while mounting the members app on a custom domain requires Premium or higher.

Before you start, prepare your Webflow site, Firebase project, local development tools, and framework knowledge.

Gather these prerequisites:

  • Webflow site: Create the site that will host the mounted app under its configured Webflow Cloud path.
  • Firebase project: Use a Google account and enable Firebase Authentication with the Email/Password provider for this build.
  • Node.js 22 or later, with npm: The bring-your-own-app page sets that floor, and Webflow Cloud supports only npm, so skip pnpm and yarn for this project.
  • TypeScript and App Router familiarity: Be ready to work with Next.js Route Handlers, client components, App Router layouts, and a middleware file.

With those pieces in place, you can configure identity first, then connect the browser, server session, and protected routes.

6 steps to add Firebase authentication in Webflow Cloud

These six steps create a mounted members app that signs users in, verifies Firebase ID tokens with Web Crypto, refreshes its cookie, and redirects unauthenticated requests before protected pages render.

The build moves from Firebase and Webflow Cloud configuration into client sign-in, server verification, session synchronization, and middleware-based route gating.

1. Configure Firebase Authentication and authorize your Webflow domain

Configure Email/Password authentication and authorize the domain that will serve the mounted app. In the Firebase console, create a project, go to Build, open Authentication, and click Get started. On the Sign-in method tab, choose Email/Password, turn the switch on, and save.

Authorized domains is worth setting now even though this build will not trip it: the list gates OAuth popup and redirect operations and continue URLs, not the email and password endpoint used here. It starts mattering as soon as you add Google or another provider.

Still in Authentication, open the Settings tab and find Authorized domains. Add the domain the members area will be served from: the domain you use for testing now, and your custom domain when you move to it.

The Webflow domain is not on the list by default, and a provider sign-in from an unlisted domain fails.

Then open Project settings (the gear icon next to Project Overview), scroll to Your apps on the General tab, and add a Web app. Copy the config object after registering the app. Its apiKey identifies the project to the client SDK.

You will also use the authDomain and projectId values. On the Users tab of Authentication, add one test user with an email and password so you have credentials to try later.

You should now see Email/Password listed as active under Sign-in method, your Webflow domain in Authorized domains, and one user in the Users list.

2. Create the Next.js app and mount it on Webflow Cloud

Create a supported Next.js project with npm for the /members mount path. The bring-your-own-app page states that Webflow Cloud supports Next.js 15 or higher, Astro 6 or 7, and Vite 6.1 or higher.

Scaffold with an explicit major, not the floating @latest tag:

npx create-next-app@15 firebase-members --typescript --app --eslint --no-src-dir --no-tailwind --import-alias "@/*" --use-npm
cd firebase-members
npm install firebase jose

The @15 is doing security work, not just compatibility work. next@latest is 16, where middleware.ts is deprecated in favor of proxy.ts, and the Next.js docs state that "Proxy defaults to using the Node.js runtime" and that "the config option is not available in Proxy files."

Webflow Cloud runs only Edge runtime middleware, so on 16 the gate in step 6 stops running and the members area opens to everyone. Within 15, stay at or above 15.2.3: earlier 15.x releases carry GHSA-f82v-jwr5-mffw, a critical authorization bypass that lets a crafted request skip middleware.

Since middleware is the only check in this build, an older pin is a bypassable gate. @15 resolves to the newest 15.x, which clears both.

--no-src-dir keeps lib/ and components/ at the project root so the @/* alias in the imports below resolves.

That gives you an App Router project with a package-lock.json, which is consistent with npm-only support.

Configuring the Webflow Cloud environment

In Webflow Cloud, configure an environment for the app with the mount path /members. Do not add basePath to next.config; Webflow Cloud injects the mount path at build time, so setting it yourself conflicts with the injected value.

In the environment variables panel, add the values you copied from Firebase:

  • NEXT_PUBLIC_FIREBASE_API_KEY: Add the apiKey that identifies the project to Firebase's browser client SDK and connects the client authentication instance to its configuration.
  • NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: Add the authDomain, usually your-project.firebaseapp.com, so that the client SDK can handle sign-in redirects and popups for the configured Firebase project.
  • NEXT_PUBLIC_FIREBASE_PROJECT_ID: Add the projectId, which points the client and server to the same Firebase project and serves as the expected token audience during verification.
  • NEXT_PUBLIC_BASE_PATH: Set the mounted app's client-side request prefix to /members so sign-in and token-refresh requests reach the session Route Handler.

Webflow Cloud makes environment variables available to both the build and the deployed app at runtime, so NEXT_PUBLIC_ values reach the client bundle and the server can read the project ID without a second copy.

After the first deploy finishes, visiting the app's deployed URL under /members should render the default Next.js page.

3. Install the Firebase and jose packages and initialize the client

Install registry-verified, explicitly pinned versions of the Firebase web SDK for the browser and jose for the server, then create a reusable Firebase client module. React Fast Refresh can initialize a module more than once during development, so the module needs to reuse an existing Firebase app.

In this build, the firebase package runs only in the browser. jose runs on the server.

Create the client module at lib/firebase-client.ts:

import { initializeApp, getApps, getApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
};

const app = getApps().length ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);

The getApps().length guard returns the existing app when one is already registered, so React Fast Refresh in next dev does not throw a duplicate-app error.

Any component that imports auth from this file now has a configured Firebase Auth instance pointed at your project.

4. Verify Firebase ID tokens in a session Route Handler

Create a shared token verifier and a session Route Handler that accepts a token only after verification, then stores it in an httpOnly cookie.

Firebase's ID token verification docs set out the full claim list: an RS256 signature from Google's current signing keys, an issuer of https://securetoken.google.com/<projectId>, an audience equal to the project ID, a non-empty sub, and exp and iat in the right direction. jose enforces exp and iat for you; the rest are passed explicitly below.

One thing to note if you open that page: it points to Google's x509 certificate URL, which suits libraries that parse certificates. Google publishes the same keys in JWK format at the service_accounts/v1/jwk path, and that is the form createRemoteJWKSet consumes.

Token verifier module

The verifier lives in one module that both the Route Handler and the middleware import:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const projectId = process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!;

const googleKeys = createRemoteJWKSet(
  new URL(
    'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com'
  )
);

export async function verifyFirebaseToken(idToken: string) {
  const { payload } = await jwtVerify(idToken, googleKeys, {
    algorithms: ['RS256'],
    issuer: `https://securetoken.google.com/${projectId}`,
    audience: projectId,
  });

  if (!payload.sub) {
    throw new Error('Token has no subject');
  }

  return payload;
}

createRemoteJWKSet fetches Google's signing keys on first use and caches them, so protected-route middleware does not hit Google on every request.

The sub claim is the Firebase user ID, which is what you would look up in your own data if you attach roles or subscription status later.

Session Route Handler

Now create the Route Handler at app/api/session/route.ts:

import { NextResponse } from 'next/server';
import { verifyFirebaseToken } from '@/lib/verify-token';

const COOKIE_NAME = 'member_session';

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);
  const idToken = body?.idToken;

  if (typeof idToken !== 'string') {
    return NextResponse.json({ error: 'Missing idToken' }, { status: 400 });
  }

  try {
    const payload = await verifyFirebaseToken(idToken);
    const response = NextResponse.json({ uid: payload.sub });

    response.cookies.set(COOKIE_NAME, idToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      path: process.env.NEXT_PUBLIC_BASE_PATH || '/',
      maxAge: 60 * 60,
    });

    return response;
  } catch {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }
}

export async function DELETE() {
  const response = NextResponse.json({ ok: true });
  response.cookies.delete(COOKIE_NAME);
  return response;
}

Leave out export const runtime = 'edge', even though Webflow Cloud guidance says to add it to API routes. The word "edge" does two jobs: Webflow Cloud runs your app on Cloudflare Workers, an edge platform, while the Next.js edge runtime target is a separate compile option that the OpenNext Cloudflare adapter doesn't support.

The directive ships a broken build; the platform already runs at the edge.

The maxAge matches the documented ID token lifetime, so the cookie and token expire together. After deploying, a POST to /members/api/session with a bad token returns a 401 JSON body, which confirms the verifier is wired in.

5. Build the login page and post the token to the session route

Build the login page, sign the member in with the Firebase client SDK, read the ID token, and post it to the mounted session route through NEXT_PUBLIC_BASE_PATH. The session request needs the mounted app's path prefix because client-side fetch calls must manually include the base path.

Here is app/login/page.tsx in full:

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth } from '@/lib/firebase-client';

const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';

export default function LoginPage() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setError(null);

    try {
      const credential = await signInWithEmailAndPassword(auth, email, password);
      const idToken = await credential.user.getIdToken();

      const response = await fetch(`${basePath}/api/session`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ idToken }),
      });

      if (!response.ok) {
        throw new Error('Session could not be created');
      }

      router.push('/dashboard');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Sign-in failed');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Email
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </label>
      <label>
        Password
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </label>
      <button type="submit">Sign in</button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

The page calls getIdToken() immediately after sign-in, so the value posted to the server is fresh. router.push('/dashboard') resolves to /members/dashboard without any prefixing on your part, because the router carries the build-time basePath.

The browser ID token and cookie need to stay synchronized, or members drop out when the current token expires.

Synchronizing client and server session

Add a small client component at components/session-sync.tsx and render it in app/layout.tsx:

'use client';

import { useEffect } from 'react';
import { onIdTokenChanged } from 'firebase/auth';
import { auth } from '@/lib/firebase-client';

const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';

export function SessionSync() {
  useEffect(() => {
    return onIdTokenChanged(auth, async (user) => {
      if (!user) return;
      const idToken = await user.getIdToken();
      await fetch(`${basePath}/api/session`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ idToken }),
      });
    });
  }, []);

  return null;
}

onIdTokenChanged fires whenever Firebase rotates the token, and each rotation re-posts to the session route, which overwrites the cookie with a fresh one.

Create a placeholder app/dashboard/page.tsx with any content, deploy, and sign in with your test user: the browser should land on /members/dashboard with a member_session cookie visible in DevTools under Application, then Cookies.

6. Gate member pages with middleware.ts

Save the protected-route guard as middleware.ts at the project root so a stale or missing session cookie redirects before a protected page renders. Webflow Cloud's framework customization guidance states that Node.js runtime middleware isn't supported and that only Edge runtime middleware works on the Workers runtime.

Newer Next.js releases rename middleware to proxy.ts, but proxy runs on the Node runtime and cannot opt into Edge, so a renamed file cannot run.

Save this as middleware.ts next to your app directory:

import { NextResponse, type NextRequest } from 'next/server';
import { verifyFirebaseToken } from '@/lib/verify-token';

const COOKIE_NAME = 'member_session';

function redirectToLogin(request: NextRequest) {
  const url = request.nextUrl.clone();
  url.pathname = '/login';
  url.searchParams.set('next', request.nextUrl.pathname);
  const response = NextResponse.redirect(url);
  response.cookies.delete(COOKIE_NAME);
  return response;
}

export async function middleware(request: NextRequest) {
  const token = request.cookies.get(COOKIE_NAME)?.value;

  if (!token) {
    return redirectToLogin(request);
  }

  try {
    await verifyFirebaseToken(token);
    return NextResponse.next();
  } catch {
    return redirectToLogin(request);
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*'],
};

Cloning request.nextUrl and setting pathname keeps the injected basePath on the redirect. The resulting destination is /members/login. The matcher lists only member routes; /login and /api/session stay open, which is what lets an unauthenticated browser reach the login form and post a token.

Cookie handling during verification failures

Deleting the cookie on a failed verification matters more than it looks. A stale cookie that stays in place would send the member into a redirect loop between a rejected dashboard request and a login page that thinks they are still signed in.

Security boundaries of this session flow:

  • Session POST: It authenticates identity only; roles, plans, subscriptions, and ownership require separate authorization, and it lacks rate limiting, spend controls, CSRF tokens, and Origin validation.
  • Session DELETE: It deletes the requesting browser's cookie without token verification or resource-level checks; it also lacks rate limiting, spend controls, CSRF protection, and Origin validation.
  • Route middleware: It skips separate revoked-token and disabled-user lookups; you need to check roles, plans, subscriptions, ownership, and other entitlements before granting paid or user-specific access.
  • Login flow: The application has no login rate limit or abuse control; the httpOnly cookie blocks direct token reads, but same-origin scripts can make authenticated requests.

You should now understand which protections this authentication flow supplies and which authorization, abuse-control, and endpoint protections still require separate implementation.

No client logout action is included. If Firebase remains signed in, SessionSync can post another token and recreate a deleted cookie, so a complete logout flow must end the Firebase client session before deleting the server cookie.

Deploy and open /members/dashboard in a private window: you should be redirected to /members/login?next=%2Fdashboard. Sign in, and the dashboard renders. Delete the cookie in DevTools and reload to return to the login page.

What causes Firebase authentication to fail on Webflow Cloud?

Firebase authentication usually fails here because of an unauthorized domain, an incompatible runtime directive, a missing mounted base path, or a browser token that was not synchronized into a fresh session cookie.

Match the visible symptom to the entries below, then inspect the named Firebase, Webflow Cloud, network, or cookie setting.

Firebase returns auth/unauthorized-domain after adding provider sign-in

Cause: The browser's current hostname is absent from Firebase Authentication's Authorized domains list. Note that this cannot happen on the email and password flow built above; the SDK raises this code only from the OAuth popup and redirect path, and its message says so: "This domain is not authorized for OAuth operations for your Firebase project." So the trigger is almost always the switch to Google or another provider.

It also appears after moving from a testing domain to a custom domain because the first hostname was authorized during setup and the replacement was not. The failure occurs before the mounted app can create a session, so inspecting the Route Handler or middleware will not expose the mismatch.

Confirm the exact hostname in the browser rather than relying on the site or project name.

Fix: Open Authentication in the Firebase console, select Settings, and compare the Authorized domains list with the browser's exact hostname. Add the testing domain you currently use and the custom domain that will serve the members area. Correct any spelling or hostname mismatch, save the setting, then reload the login page and retry.

A successful attempt should proceed past Firebase sign-in and send the ID token to /members/api/session.

The deploy fails after adding export const runtime = 'edge' to the session route

Cause: The deployment is compiling a Next.js runtime directive that the OpenNext Cloudflare adapter does not support. The terminology is easy to misread because Webflow Cloud runs on Cloudflare Workers, which is an edge platform, while the Next.js edge runtime target is a separate compile option.

The app already deploys to Workers without that export, so adding it to a Route Handler or page creates a build conflict instead of enabling the platform runtime.

Fix: Remove export const runtime = 'edge' from every Route Handler and page, commit the change, and redeploy. If the build output also mentions node:fs, inspect dependencies added to the project. Workers has no filesystem, and Webflow's own Node.js compatibility table lists fs under "No file system access; use external storage."

The session route should rely on Web Crypto-compatible packages such as jose and let Webflow Cloud supply the Workers runtime.

POST /api/session returns 404

Cause: The browser called the bare API path outside the mounted /members application. Webflow Cloud injects the mount path for the deployed app, but client-side fetch calls must include that prefix manually.

The problem can remain hidden under next dev because local development has no mount path. In the Network tab, a request to /api/session instead of /members/api/session confirms that the client bundle lacks the expected base-path value.

Fix: Compare the login request and the SessionSync request with the expected fetch(${basePath}/api/session) construction. Confirm that NEXT_PUBLIC_BASE_PATH is set to /members in the Webflow Cloud environment.

Because this public variable is read at build time, redeploy after adding or changing it. The next sign-in or token refresh should send the request beneath the mounted path and reach the Route Handler.

Members are redirected to login when the Firebase token expires

Cause: Firebase refreshed the browser's ID token, but the member_session cookie still contains the expired value. Middleware verifies the cookie before rendering a protected page, so it rejects the stale token, deletes the cookie, and redirects the member to the login route.

This usually means SessionSync is missing from app/layout.tsx, its listener did not run, or its client-side request did not reach the mounted session endpoint.

Fix: Leave the dashboard open until token rotation and use the Network tab to watch for a new POST to /members/api/session. Confirm that onIdTokenChanged receives a signed-in user, calls getIdToken(), and posts through NEXT_PUBLIC_BASE_PATH.

Inspect the cookie afterward to verify that the session route overwrote it. Staying signed in across browser restarts beyond a token lifetime requires a server-managed session with its own store, which this build deliberately does not add.

What you can build next with Firebase and Webflow

Once a member can sign in with Firebase Authentication on your Webflow site and the server enforces it, the rest of a membership product is data attached to that verified sub: a Firestore document per member holding plan status, and a Route Handler that reads it before rendering paid content. A webhook from your billing provider is what flips the flag when a subscription changes.

If members need different access levels rather than one gate, copy the role-based access pattern. I would add provider sign-in first, though, because it is the smallest extension.

Frequently asked questions

Does this protect pages I designed in the Webflow Designer?

You protect only routes inside the mounted app because that is where the middleware runs. Webflow Designer pages remain public, so you can keep marketing and pricing content there while placing member-only screens under the mounted path. Point a Designer button to /members/dashboard to hand sign-in visitors from the public site to the protected application.

Can I test the whole flow locally before deploying to Webflow Cloud?

You can test sign-in, the session route, and middleware under next dev. Leave NEXT_PUBLIC_BASE_PATH empty in .env.local, since no mount path exists locally. Use the Firebase configuration for the same project. Remember that local development uses Node rather than Workers, so deploy before treating runtime compatibility as confirmed on Webflow Cloud.

Where should I keep Firebase secrets if I add Firestore reads later?

You should store Firebase server credentials as secret environment variables in the Webflow Cloud environment without the NEXT_PUBLIC_ prefix. That keeps them out of the browser bundle while making them available to deployed server code. Webflow also redacts secrets from build logs. Read those values only inside Route Handlers or middleware, never inside client components.


Last Updated
September 25, 2026
Category

Related articles


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.