Firebase webframeworks Not Enabled in GitHub Actions: Fix
DevOps

Firebase webframeworks Not Enabled in GitHub Actions: Fix

Running firebase experiments:enable webframeworks on your laptop does nothing for GitHub Actions. Here is the env var the CLI actually reads, the complete workflow, and why Firebase now steers Next.js apps to App Hosting.

8 min read
Firebase webframeworks Not Enabled in GitHub Actions: Fix

Photo by imgix on Unsplash

Deploying a Next.js app to Firebase Hosting works on your laptop and then dies in CI with one line:

text
Error: Cannot deploy a web framework to hosting because the experiment webframeworks is not enabled. To enable webframeworks run firebase experiments:enable webframeworks

That is exactly the symptom in the Stack Overflow question How can run firebase experiments:enable webframeworks with GitHub actions? (20 votes, ~9k views). The asker had already run firebase experiments:enable webframeworks locally, was using FirebaseExtended/action-hosting-deploy@v0 with a service account, and still hit the error on every push to main. The accepted answer (46 votes) is a single environment variable. Below is why that variable is the only thing that works, the full workflow, and the parts of the picture that have changed since 2022, including Firebase closing the experiment to new projects.

Why enabling the experiment locally does nothing in CI#

firebase experiments:enable <name> does not touch firebase.json. In the CLI source (src/experiments.ts), setEnabled() writes to localPreferences(), which is the previews key in the CLI's configstore, a per-user file on the machine that ran the command. isEnabled() reads the same place, falling back to the experiment's default, and webframeworks is declared public: true with no default, so it is off unless something turns it on.

A GitHub Actions runner is a fresh machine with an empty configstore. Nothing you enabled on your laptop travels with the repo. The fix has to happen inside the job.

The CLI knows this. The same file contains enableExperimentsFromCliEnvVariable(), which reads a comma-delimited FIREBASE_CLI_EXPERIMENTS environment variable and silently enables every valid experiment listed in it. assertEnabled() even detects GitHub Actions (isRunningInGithubAction()) and prints a message telling you to add that variable to your workflow file. The asker saw the generic message because the CLI version on the runner predated that change.

The fix: FIREBASE_CLI_EXPERIMENTS on the deploy step#

The accepted answer, verbatim in spirit:

yaml
- uses: FirebaseExtended/action-hosting-deploy@v0
  with:
    # ...
  env:
    FIREBASE_CLI_EXPERIMENTS: webframeworks

The feature landed in firebase-tools#5069 and shipped in firebase-tools v11.14.2. Anything older will ignore the variable and keep failing, which matters if you pin firebaseToolsVersion in the action.

Complete GitHub Actions workflow for a Next.js app#

This is the asker's workflow, corrected and brought up to date. It uses the service-account path that firebase init hosting:github generates for you, not a login token.

yaml
name: Deploy to Firebase Hosting on merge
on:
  push:
    branches:
      - main
 
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
 
      - run: npm ci
 
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          channelId: live
          projectId: my-project
        env:
          FIREBASE_CLI_EXPERIMENTS: webframeworks

Three details that differ from the original question:

  1. No separate npm run build. With web frameworks enabled, firebase deploy runs the Next.js build itself, decides which routes are static and which need getServerSideProps or Image Optimization, and provisions a Cloud Function for the dynamic part. Building first is harmless but doubles your CI time.
  2. FIREBASE_SERVICE_ACCOUNT, not FIREBASE_TOKEN. The firebase-tools README marks firebase login:ci tokens as deprecated: "this authentication method will be removed in a future major version of firebase-tools; use a service account to authenticate instead." The firebaseServiceAccount input is the supported route, and firebase init hosting:github creates the service account, encrypts the key and uploads it as a repository secret for you.
  3. Node 20. Current firebase-tools (15.x) declares engines.node >=20.0.0 || >=22.0.0 || >=24.0.0. The frameworks code picks the SSR runtime from VALID_ENGINES.node = [20, 22, 24] by taking the highest entry that is at or below the Node version running the CLI. Pin setup-node to the same major you run locally so the function runtime does not change underneath you.

If you call the CLI directly instead of the action#

The variable is read by the CLI itself, so it works with a plain run step too:

yaml
- run: npm i -g firebase-tools
- run: firebase deploy --only hosting --project my-project --non-interactive
  env:
    FIREBASE_CLI_EXPERIMENTS: webframeworks
    GOOGLE_APPLICATION_CREDENTIALS: ${{ github.workspace }}/sa.json

Write the service-account JSON to sa.json in an earlier step from a secret, and never commit it. GOOGLE_APPLICATION_CREDENTIALS is the mechanism the README recommends for CI. If you are still on FIREBASE_TOKEN, it continues to work today via --token or the env var, but plan the migration.

Adding FIREBASE_CLI_EXPERIMENTS as a step in the workflow that runs firebase experiments:enable webframeworks before deploy also works, because both commands run on the same runner in the same job. The env var is shorter and survives a switch to the action.

Pitfall: the SSR function lands in us-central1 unless you say otherwise#

The frameworks deploy path reads frameworksBackend?.region ?? DEFAULT_REGION, and DEFAULT_REGION is "us-central1" (src/frameworks/constants.ts). Your CDN edge is global, but every server-rendered request from Paris makes a round trip to Iowa.

The override lives in firebase.json, next to source, which is the key that tells the CLI to treat the directory as a framework project instead of a static public folder:

json
{
  "hosting": {
    "source": ".",
    "frameworksBackend": {
      "region": "europe-west1",
      "memory": "512MiB",
      "maxInstances": 10
    }
  }
}

europe-west1 (Belgium) is a Tier 1 Cloud Functions region available on both generations, which keeps pricing at the lower tier. europe-west4 (Netherlands), europe-west8 (Milan) and europe-west9 (Paris) are also Tier 1 but 2nd-gen only; europe-west2 (London) and europe-west3 (Frankfurt) are Tier 2, so cost more per invocation. The full list is on the Cloud Functions locations page. The frameworksBackend schema also accepts minInstances, concurrency, timeoutSeconds, secrets and cpu, per the CLI's firebase-config.json schema.

Changing the region after a first deploy creates a new function; delete the old one from the console or you pay for an idle us-central1 instance if you set minInstances.

Pitfall: billing, hosting.source, and CLI version#

  • SSR needs the Blaze plan. The frameworks overview lists billing as optional "required if you plan to use SSR". A Spark-plan project deploys static output fine and fails the moment the CLI tries to create the function.
  • hosting.source vs hosting.public. If your firebase.json still has "public": "out" from an old static export, the CLI will not run the framework pipeline at all and you will deploy whatever happens to be in that folder. Run firebase init hosting again and answer yes to using a web framework, or edit the file by hand.
  • Firebase CLI 12.1.0 or later is the documented floor for framework-aware Hosting. action-hosting-deploy defaults to the latest CLI unless you set firebaseToolsVersion, so an old pin is the usual reason a working workflow regresses.
  • Environment variables. The CLI honours .env, .env.<PROJECT_ID> and similar dotenv files at build time, so NEXT_PUBLIC_* values need to be present on the runner before the deploy step. The failure mode is identical to the one in Next.js environment variables undefined on Vercel: the values are inlined at build, and a missing secret produces a broken bundle, not an error. Keep the client-side Firebase config there too; it is safe to expose, as covered in Is it safe to expose the Firebase API key?, but the admin service account JSON is not.

The 2026 state: the experiment is closed to new projects#

This is the part that changes the advice for anyone starting today. The Next.js integration page now carries two notices:

Framework-aware Hosting is an early public preview. This means that the functionality might change in backward-incompatible ways.

For Next.js developers, new participation in the Hosting frameworks experiment has been closed permanently. If you're already using the frameworks experiment in the Firebase CLI, we recommend "graduating" to App Hosting.

The webframeworks experiment is still present in the CLI source, its fullDescription still warns that "a manual migration may be required when the non-experimental support for these frameworks is released", and the env-var fix still works for projects already using it. But Firebase's own positioning is that Next.js belongs on App Hosting, which needs Next.js 13.5 or later, the Blaze plan, and a GitHub connection made from the console. App Hosting builds on Cloud Run, rolls out on every commit to the branch you choose, and needs no experiment flag and no action-hosting-deploy step at all.

The EU angle is worth checking before you move. App Hosting launched in us-central1 and added europe-west4 (Netherlands) in late 2024; europe-west1 is not in its region list at the time of writing, whereas the Hosting-plus-Functions path lets you pick any Cloud Functions region. If Belgium residency was a requirement, verify the current App Hosting location list before migrating. The --location flag on firebase apphosting:backends:create is where you set it.

Firebase Hosting vs Vercel vs Cloudflare Pages for the same Next.js app#

The mechanical difference is who runs the build and what the server half executes on.

Firebase Hosting (frameworks)Firebase App HostingVercelCloudflare Pages
Server runtimeCloud Functions (Node 20/22/24)Cloud RunNode or Edge functionsWorkers runtime only
Region choiceAny Functions region via frameworksBackend.regionBackend location at create timeProject function-region settingGlobal by default
CI setupYour workflow + FIREBASE_CLI_EXPERIMENTSConsole GitHub connectionGit integrationGit integration or Wrangler
StatusPreview, closed to new projectsFirebase's recommended pathStableStable

Vercel is still the zero-config path, and its failure modes are documented on this site: middleware not running in production and the environment-variable inlining above. Cloudflare Pages is the cheapest at scale but forces the Workers runtime on every server-side route, which rules out Node-only dependencies; the trade-offs are in Serverless edge computing. If your data layer is Supabase rather than Firestore, the end-to-end setup, including connection pooling from serverless functions, is in Deploying Next.js + Supabase to production, and the platform-level comparison is in Supabase vs Firebase in 2026.

Quick checklist#

  1. Deploy step has env: FIREBASE_CLI_EXPERIMENTS: webframeworks.
  2. firebase-tools is at least 11.14.2 for the variable, 12.1.0 for framework-aware Hosting; unpinned is fine.
  3. Auth is firebaseServiceAccount or GOOGLE_APPLICATION_CREDENTIALS, not a login:ci token.
  4. firebase.json uses hosting.source, not hosting.public.
  5. hosting.frameworksBackend.region is set to your EU region; default is us-central1.
  6. Blaze plan is active if any route is server-rendered or uses next/image.
  7. setup-node major matches the runtime you want for the SSR function.
  8. For a new project, evaluate App Hosting first; the Hosting experiment is closed to new participants.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.