Firebase Hosting supports single-page applications out of the box. Add a rewrite rule in firebase.json that sends all URL paths to index.html, configure your build output directory, and deploy with the Firebase CLI. This ensures client-side routing works correctly and users never see a 404 on valid routes.
Serving a Single-Page Application on Firebase Hosting
Single-page applications handle routing on the client side, which means the server must return index.html for every URL path. Firebase Hosting makes this straightforward with a rewrite rule in firebase.json. This tutorial walks you through initializing Firebase Hosting, configuring the SPA rewrite, setting your framework's build output directory, and deploying with preview channels for safe testing.
Prerequisites
- A Firebase project created in the Firebase Console
- Firebase CLI installed globally (npm install -g firebase-tools) and logged in
- A built SPA with an index.html entry point (e.g., npm run build output)
- Node.js 18 or later installed on your machine
Step-by-step guide
Initialize Firebase Hosting in your project
Initialize Firebase Hosting in your project
Run firebase init hosting in your project root. The CLI asks which Firebase project to use, what directory contains your built files, and whether to configure as a single-page app. Select yes for the SPA question — this automatically adds the rewrite rule. For React (Create React App or Vite), the build directory is typically build or dist. For Angular, it is dist/your-project-name. For Vue, it is dist.
1firebase init hosting23# When prompted:4# ? What do you want to use as your public directory? dist5# ? Configure as a single-page app (rewrite all urls to /index.html)? Yes6# ? Set up automatic builds and deploys with GitHub? NoExpected result: A firebase.json file is created with a hosting section that includes a rewrite rule pointing all routes to index.html.
Configure firebase.json with SPA rewrites and clean URLs
Configure firebase.json with SPA rewrites and clean URLs
Open firebase.json and verify the hosting configuration. The rewrites array should contain a catch-all rule that sends every URL to index.html. Enable cleanUrls to strip .html extensions from URLs. Add custom headers for cache control — static assets like JavaScript and CSS should be cached aggressively, while index.html should never be cached so users always get the latest version.
1{2 "hosting": {3 "public": "dist",4 "cleanUrls": true,5 "trailingSlash": false,6 "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],7 "headers": [8 {9 "source": "/index.html",10 "headers": [11 { "key": "Cache-Control", "value": "no-cache" }12 ]13 },14 {15 "source": "/assets/**",16 "headers": [17 { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }18 ]19 }20 ],21 "rewrites": [22 { "source": "**", "destination": "/index.html" }23 ]24 }25}Expected result: Your firebase.json has a complete hosting configuration with rewrites, headers, and clean URL settings.
Add a custom 404 page for missing assets
Add a custom 404 page for missing assets
While the SPA rewrite handles route URLs, requests for missing static files (images, scripts) should return a meaningful error. Create a 404.html file in your public directory. Firebase Hosting automatically serves this file for any request that does not match a real file and is not caught by a rewrite. For most SPAs, you want the rewrite to handle page routes and the 404 to handle genuinely missing files.
1<!-- dist/404.html -->2<!DOCTYPE html>3<html lang="en">4<head>5 <meta charset="UTF-8">6 <meta name="viewport" content="width=device-width, initial-scale=1.0">7 <title>Page Not Found</title>8 <style>9 body { font-family: system-ui; text-align: center; padding: 60px 20px; }10 h1 { font-size: 2rem; margin-bottom: 1rem; }11 a { color: #1a73e8; text-decoration: none; }12 </style>13</head>14<body>15 <h1>404 — Page Not Found</h1>16 <p>The resource you requested does not exist.</p>17 <a href="/">Return to Home</a>18</body>19</html>Expected result: A 404.html file exists in your public directory and displays a friendly error message for missing static assets.
Build your SPA and verify the output directory
Build your SPA and verify the output directory
Run your framework's build command and confirm the output matches the public directory in firebase.json. The build directory must contain an index.html at the root level. If your framework outputs to a different directory (like build instead of dist), update the public field in firebase.json to match. A common mistake is deploying the source directory instead of the build output.
1# React (Vite)2npm run build3# Output: dist/45# React (Create React App)6npm run build7# Output: build/89# Angular10ng build --configuration production11# Output: dist/your-project-name/1213# Vue (Vite)14npm run build15# Output: dist/1617# Verify index.html exists18ls dist/index.htmlExpected result: The build directory contains index.html and all bundled assets ready for deployment.
Deploy to a preview channel for testing
Deploy to a preview channel for testing
Before deploying to production, use a preview channel to test your SPA on a temporary URL. Preview channels create an isolated deployment that expires after a configurable period. This lets you verify that routing, assets, and caching all work correctly without affecting your live site. Share the preview URL with teammates for review.
1# Deploy to a preview channel named "staging"2firebase hosting:channel:deploy staging --expires 7d34# Output:5# ✔ hosting:channel: Channel URL: https://your-project--staging-abc123.web.app6# Preview channel expires in 7 daysExpected result: A temporary preview URL is created where you can test all SPA routes and verify client-side routing works correctly.
Deploy to production
Deploy to production
Once you have verified the preview deployment, deploy to production with firebase deploy --only hosting. This pushes your built SPA to Firebase's global CDN. The deployment is atomic — the new version replaces the old one instantly with no downtime. After deployment, test your production URL by navigating directly to a deep route (like /about or /dashboard) to confirm the rewrite is serving index.html correctly.
1# Deploy to production2firebase deploy --only hosting34# Output:5# ✔ Deploy complete!6# Hosting URL: https://your-project.web.app78# Test deep linking by navigating directly to a route9# https://your-project.web.app/about should load your SPA, not return a 404Expected result: Your SPA is live on Firebase Hosting. All routes serve index.html, client-side routing works, and static assets are cached by the CDN.
Complete working example
1{2 "hosting": {3 "public": "dist",4 "cleanUrls": true,5 "trailingSlash": false,6 "ignore": [7 "firebase.json",8 "**/.*",9 "**/node_modules/**"10 ],11 "headers": [12 {13 "source": "/index.html",14 "headers": [15 {16 "key": "Cache-Control",17 "value": "no-cache"18 }19 ]20 },21 {22 "source": "/assets/**",23 "headers": [24 {25 "key": "Cache-Control",26 "value": "public, max-age=31536000, immutable"27 }28 ]29 },30 {31 "source": "**/*.@(js|css)",32 "headers": [33 {34 "key": "Cache-Control",35 "value": "public, max-age=31536000, immutable"36 }37 ]38 },39 {40 "source": "**",41 "headers": [42 {43 "key": "X-Content-Type-Options",44 "value": "nosniff"45 },46 {47 "key": "X-Frame-Options",48 "value": "DENY"49 },50 {51 "key": "X-XSS-Protection",52 "value": "1; mode=block"53 }54 ]55 }56 ],57 "rewrites": [58 {59 "source": "/api/**",60 "function": "api"61 },62 {63 "source": "**",64 "destination": "/index.html"65 }66 ]67 }68}Common mistakes when hosting a Single-Page Application With Firebase Hosting
Why it's a problem: Setting the public directory to the source folder instead of the build output (e.g., src instead of dist)
How to avoid: Always set the public field in firebase.json to your framework's build output directory: dist for Vite/Vue, build for Create React App, or dist/project-name for Angular.
Why it's a problem: Forgetting to build before deploying, which uploads stale or missing files
How to avoid: Run your build command (npm run build) before every firebase deploy. Consider adding a predeploy script in firebase.json to automate this.
Why it's a problem: Caching index.html aggressively, which prevents users from seeing updated deployments
How to avoid: Set Cache-Control: no-cache on index.html so browsers always check for updates. Cache static assets with hashed filenames aggressively instead.
Why it's a problem: Placing the SPA rewrite before API function rewrites, which prevents Cloud Functions from receiving requests
How to avoid: List specific rewrites (like /api/** to a Cloud Function) before the catch-all ** rewrite. Firebase Hosting evaluates rewrites in order and uses the first match.
Best practices
- Always build your SPA before deploying — add a predeploy hook in firebase.json to automate the build step
- Set Cache-Control: no-cache on index.html and cache hashed static assets with max-age=31536000 and immutable
- Use preview channels to test deployments before pushing to production
- Add security headers (X-Content-Type-Options, X-Frame-Options) in the headers section of firebase.json
- Place specific rewrites (API routes, Cloud Functions) before the catch-all SPA rewrite
- Enable cleanUrls to remove .html extensions and give your SPA cleaner URL paths
- Use the ignore array to exclude firebase.json, dotfiles, and node_modules from deployment
- Test deep linking by navigating directly to a route URL after deployment to confirm rewrites work
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a React SPA built with Vite that outputs to a dist folder. Write a complete firebase.json configuration for Firebase Hosting that rewrites all routes to index.html, sets aggressive caching for static assets but no-cache for index.html, adds security headers, and supports an API Cloud Function at /api/**.
Configure my Firebase Hosting project for a single-page application. Set the public directory to dist, add a catch-all rewrite to index.html, enable clean URLs, set Cache-Control headers for index.html (no-cache) and static assets (immutable, 1 year), and add X-Content-Type-Options and X-Frame-Options security headers.
Frequently asked questions
Why do I get a 404 when I refresh a page in my SPA on Firebase Hosting?
Firebase Hosting does not know about your client-side routes. Without a rewrite rule, it looks for a file matching the URL path and returns 404 when none exists. Add a catch-all rewrite in firebase.json: {"source": "**", "destination": "/index.html"} to fix this.
Does the SPA rewrite affect requests for static assets like images and CSS files?
No. Firebase Hosting serves existing files first. The rewrite only applies when no file matches the requested path. Your images, CSS, JavaScript, and other static assets are served directly from the CDN.
Can I use Firebase Hosting with a server-rendered framework like Next.js?
For SSR frameworks, use Firebase App Hosting instead of standard Firebase Hosting. App Hosting supports Next.js and Angular with GitHub-based CI/CD and serverless rendering. Standard Firebase Hosting is designed for static files and SPAs.
How do I set up environment variables for my SPA on Firebase Hosting?
Firebase Hosting serves static files, so environment variables must be baked into the build. Use your framework's .env file (e.g., VITE_API_URL for Vite) and run the build step with those values set. Do not put secrets in client-side environment variables.
Is Firebase Hosting free for SPAs?
The Spark (free) plan includes 10 GB of storage and 360 MB/day of bandwidth. This is sufficient for most small SPAs. The Blaze plan charges $0.026/GB for storage and $0.15/GB for bandwidth beyond the free tier.
Can I use a custom domain with Firebase Hosting?
Yes. In the Firebase Console, go to Hosting and click Add custom domain. You will need to add DNS A records pointing to Firebase's IP addresses. SSL is automatically provisioned once DNS propagates, which typically takes a few hours.
Can RapidDev help configure Firebase Hosting for my SPA?
Yes. RapidDev can set up your Firebase Hosting configuration including SPA rewrites, caching strategies, custom domains, and CI/CD pipelines so your deployments are fast and reliable.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation