Firebase Hosting skill for Codex
Static sites, API rewrites and preview channels. Codex writes firebase.json so hashed assets cache for a year, HTML revalidates, and /api routes reach a 2nd gen function or Cloud Run service on the same domain. It knows the __session cookie rule and when the CDN caches function responses, sets up preview channels per pull request, always scopes deploys with --only, and checks the active project alias before anything goes live.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- firebase-hosting Installs to ~/.agents/skills/firebase-hosting/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install firebase-hosting The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project firebase-hosting Installs to .agents/skills/firebase-hosting/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/firebase-hosting.zipunzip -o firebase-hosting.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) firebase-hosting PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $firebase-hosting, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Configures and deploys Firebase Hosting - firebase.json public directory, rewrites to Cloud Functions (2nd gen) and Cloud Run, SPA fallbacks, redirects, headers and cache control, cleanUrls, multiple sites and deploy targets, preview channels for pull requests, the local emulator, GitHub Actions deploys, rollback and the difference from Firebase App Hosting. Use when deploying a static or SPA site to Firebase, wiring an API behind the same domain, rewrites return 404, function responses are not cached, or setting up per-PR preview URLs.
Use it when
- Deploying a static site or SPA to Firebase Hosting
- Routing API paths to Cloud Functions or Cloud Run behind Hosting
- Function responses through Hosting are slow or never cached
- Setting up preview channels or multiple Hosting sites
Use something else when
- You are writing the Cloud Functions themselves beyond the Hosting wiring: use gcp-cloud-functions
- You need Firebase Authentication flows: use firebase-auth
How to check the result
The emulator and a preview channel serve correct routes and rewrites, curl -sI shows the intended Cache-Control, and firebase use names the right project before deploy.
The mistake it prevents
Running firebase deploy without --only, which also redeploys functions, security rules and indexes along with the site.
A clean Firebase Hosting setup serves hashed assets with long cache lifetimes and HTML
with short ones, routes /api/** to a function or Cloud Run service in the same project,
gives every pull request its own expiring preview channel, and deploys --only hosting
so a site change never redeploys unrelated functions or rules.
Inspect first
- Read
AGENTS.md,firebase.json,.firebaserc(project aliases and targets), the framework build output directory, andfunctions/or Cloud Run source if present. - Run
firebase useto see the active project alias. Deploying to the wrong alias is the most common incident.firebasecommands that touch projects need network and login (firebase login, or a service account in CI). - Decide the product:
| App | Product |
|---|---|
| Static site, SPA (React, Vue, Angular CSR), docs | Firebase Hosting |
| Static site plus a small API | Hosting with rewrites to Cloud Functions or Cloud Run |
| Next.js or Angular with SSR as the main app | Firebase App Hosting (apphosting.yaml, Git-based rollouts), not classic Hosting |
| Container you already run on Cloud Run | Hosting rewrite to Cloud Run for the custom domain and CDN |
firebase.json baseline
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"cleanUrls": true,
"trailingSlash": false,
"redirects": [
{ "source": "/blog/:slug", "destination": "/articles/:slug", "type": 301 }
],
"rewrites": [
{ "source": "/api/**", "function": { "functionId": "api", "region": "us-central1" } },
{ "source": "/render/**", "run": { "serviceId": "renderer", "region": "europe-west1" } },
{ "source": "**", "destination": "/index.html" }
],
"headers": [
{
"source": "**/*.@(js|css|woff2|png|jpg|webp|avif|svg)",
"headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
},
{
"source": "**/*.html",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
}
]
}
}
Rules:
- Hosting serves an existing static file before evaluating rewrites, so the
**SPA fallback is safe. Redirects are evaluated before files. - Rewrites match in order; put specific API rules before the catch-all.
- Long cache headers only on hashed filenames. Unhashed assets with a one-year
immutableheader cannot be updated in browsers. - The
regionin a function rewrite must match where the function is deployed.
API behind Hosting
// functions/src/index.ts
import { onRequest } from "firebase-functions/v2/https";
import express from "express";
const app = express();
app.get("/api/products", async (_req, res) => {
res.set("Cache-Control", "public, max-age=60, s-maxage=300");
res.json(await listProducts());
});
export const api = onRequest({ region: "us-central1", memory: "512MiB", concurrency: 80 }, app);
- The Express route must include the
/apiprefix: Hosting forwards the full path. - Hosting’s CDN caches function and Cloud Run responses only when they send
Cache-Controlwithpublicands-maxage(ormax-age). Without it every request invokes the function. - Cookies are stripped from requests forwarded to functions and Cloud Run, except one
named
__session. Use that name for the session cookie. - Requests proxied through Hosting have a fixed timeout around one minute even if the function allows longer; long work must be asynchronous.
Multiple sites and targets
firebase target:apply hosting marketing acme-marketing
firebase target:apply hosting app acme-app
{
"hosting": [
{ "target": "marketing", "public": "apps/marketing/dist" },
{ "target": "app", "public": "apps/web/dist", "rewrites": [{ "source": "**", "destination": "/index.html" }] }
]
}
firebase deploy --only hosting:app
Local loop and deploys
firebase emulators:start --only hosting,functions # serves on localhost; works offline after install
firebase hosting:channel:deploy pr-123 --expires 7d # preview URL, does not touch live
firebase hosting:channel:list
firebase deploy --only hosting -m "release 2026-10-28" # live: confirm with the user first
firebase hosting:clone acme-app:pr-123 acme-app:live # promote a tested preview to live (confirm first)
Preview channels share the project’s backend (Firestore, Functions). A preview is not an
isolated staging environment; use a separate Firebase project for that and switch with
firebase use staging.
Rollback: in the console’s Hosting release history choose “Rollback”, or clone a known
good channel or version to live. Only static content and config roll back; functions
and databases do not.
GitHub Actions
firebase init hosting:github creates workflows that deploy a preview channel per PR and
live on merge using FirebaseExtended/action-hosting-deploy. Review the generated
service account: it gets deploy permissions to the project, so store its key as a
repository secret, or prefer Workload Identity Federation with
google-github-actions/auth to avoid long-lived keys.
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
projectId: acme-app
expires: 7d
Debug table
| Symptom | Check |
|---|---|
| Deep link returns 404 | Missing ** rewrite to /index.html, or wrong public directory |
/api returns the SPA HTML | API rewrite after the catch-all, or function name or region mismatch |
Function returns 404 for /api/x | Express routes defined without the /api prefix |
| Logged-in user loses session through Hosting | Cookie not named __session |
| Function invoked on every request despite caching | No public plus s-maxage in the response |
| Old site after deploy | Long max-age on HTML or unhashed assets; a stale service worker |
Verification
-
firebase emulators:startserves the site; routes, rewrites and headers behave as expected. - A preview channel URL passes smoke tests;
curl -sIshows intendedCache-Control. -
firebase useshows the intended project before every deploy. - Live deploys and clone-to-live happen only after explicit user approval.
Gotchas
firebase deploywithout--onlydeploys everything configured: functions, rules, indexes. Always scope it.- Hosting custom domains need DNS records verified; certificate provisioning can take a while after DNS changes.
- The
ignorelist does not stop secrets in the build output from being published. Auditpublicfor.envfiles and source maps you do not want public. - Each Hosting site keeps a capped number of releases; older ones are deleted automatically, so long-term rollback relies on rebuilding from git.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Firebase Hosting"
short_description: "Static sites, API rewrites and preview channels"
default_prompt: "$firebase-hosting Deploy our Vite React app to Firebase Hosting with an Express API behind /api" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.