Deploying a Next.js app to Firebase Hosting works on your laptop and then dies in CI with one line:
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:
- 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.
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:
-
No separate
npm run build. With web frameworks enabled,firebase deployruns the Next.js build itself, decides which routes are static and which needgetServerSidePropsor Image Optimization, and provisions a Cloud Function for the dynamic part. Building first is harmless but doubles your CI time. -
FIREBASE_SERVICE_ACCOUNT, notFIREBASE_TOKEN. The firebase-tools README marksfirebase login:citokens as deprecated: "this authentication method will be removed in a future major version of firebase-tools; use a service account to authenticate instead." ThefirebaseServiceAccountinput is the supported route, andfirebase init hosting:githubcreates the service account, encrypts the key and uploads it as a repository secret for you. -
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 fromVALID_ENGINES.node = [20, 22, 24]by taking the highest entry that is at or below the Node version running the CLI. Pinsetup-nodeto 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:
- 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:
{
"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.sourcevshosting.public. If yourfirebase.jsonstill 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. Runfirebase init hostingagain 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-deploydefaults to the latest CLI unless you setfirebaseToolsVersion, 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, soNEXT_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 Hosting | Vercel | Cloudflare Pages | |
|---|---|---|---|---|
| Server runtime | Cloud Functions (Node 20/22/24) | Cloud Run | Node or Edge functions | Workers runtime only |
| Region choice | Any Functions region via frameworksBackend.region
|
Backend location at create time | Project function-region setting | Global by default |
| CI setup | Your workflow + FIREBASE_CLI_EXPERIMENTS
|
Console GitHub connection | Git integration | Git integration or Wrangler |
| Status | Preview, closed to new projects | Firebase's recommended path | Stable | Stable |
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
- Deploy step has
env: FIREBASE_CLI_EXPERIMENTS: webframeworks. - firebase-tools is at least 11.14.2 for the variable, 12.1.0 for framework-aware Hosting; unpinned is fine.
- Auth is
firebaseServiceAccountorGOOGLE_APPLICATION_CREDENTIALS, not alogin:citoken. -
firebase.jsonuseshosting.source, nothosting.public. -
hosting.frameworksBackend.regionis set to your EU region; default isus-central1. - Blaze plan is active if any route is server-rendered or uses
next/image. -
setup-nodemajor matches the runtime you want for the SSR function. - For a new project, evaluate App Hosting first; the Hosting experiment is closed to new participants.
Related
- Next.js Environment Variables Undefined on Vercel? Fix
- Next.js Middleware Not Running on Vercel: 3 Causes
- Deploying Next.js + Supabase to Production
- Is It Safe to Expose Firebase API Key to the Public?
- Fix: No Firebase App '[DEFAULT]' has been created
Originally published at https://www.iloveblogs.blog
Top comments (0)