# How to Host a Single-Page Application With Firebase Hosting

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Firebase Hosting (Spark and Blaze plans), any SPA framework (React, Vue, Angular, Svelte)
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

```
firebase init hosting

# When prompted:
# ? What do you want to use as your public directory? dist
# ? Configure as a single-page app (rewrite all urls to /index.html)? Yes
# ? Set up automatic builds and deploys with GitHub? No
```

**Expected result:** A firebase.json file is created with a hosting section that includes a rewrite rule pointing all routes to index.html.

### 2. 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.

```
{
  "hosting": {
    "public": "dist",
    "cleanUrls": true,
    "trailingSlash": false,
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "headers": [
      {
        "source": "/index.html",
        "headers": [
          { "key": "Cache-Control", "value": "no-cache" }
        ]
      },
      {
        "source": "/assets/**",
        "headers": [
          { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
        ]
      }
    ],
    "rewrites": [
      { "source": "**", "destination": "/index.html" }
    ]
  }
}
```

**Expected result:** Your firebase.json has a complete hosting configuration with rewrites, headers, and clean URL settings.

### 3. 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.

```
<!-- dist/404.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Page Not Found</title>
  <style>
    body { font-family: system-ui; text-align: center; padding: 60px 20px; }
    h1 { font-size: 2rem; margin-bottom: 1rem; }
    a { color: #1a73e8; text-decoration: none; }
  </style>
</head>
<body>
  <h1>404 — Page Not Found</h1>
  <p>The resource you requested does not exist.</p>
  <a href="/">Return to Home</a>
</body>
</html>
```

**Expected result:** A 404.html file exists in your public directory and displays a friendly error message for missing static assets.

### 4. 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.

```
# React (Vite)
npm run build
# Output: dist/

# React (Create React App)
npm run build
# Output: build/

# Angular
ng build --configuration production
# Output: dist/your-project-name/

# Vue (Vite)
npm run build
# Output: dist/

# Verify index.html exists
ls dist/index.html
```

**Expected result:** The build directory contains index.html and all bundled assets ready for deployment.

### 5. 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.

```
# Deploy to a preview channel named "staging"
firebase hosting:channel:deploy staging --expires 7d

# Output:
# ✔ hosting:channel: Channel URL: https://your-project--staging-abc123.web.app
# Preview channel expires in 7 days
```

**Expected result:** A temporary preview URL is created where you can test all SPA routes and verify client-side routing works correctly.

### 6. 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.

```
# Deploy to production
firebase deploy --only hosting

# Output:
# ✔ Deploy complete!
# Hosting URL: https://your-project.web.app

# Test deep linking by navigating directly to a route
# https://your-project.web.app/about should load your SPA, not return a 404
```

**Expected 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 code example

File: `firebase.json`

```json
{
  "hosting": {
    "public": "dist",
    "cleanUrls": true,
    "trailingSlash": false,
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "headers": [
      {
        "source": "/index.html",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache"
          }
        ]
      },
      {
        "source": "/assets/**",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      },
      {
        "source": "**/*.@(js|css)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      },
      {
        "source": "**",
        "headers": [
          {
            "key": "X-Content-Type-Options",
            "value": "nosniff"
          },
          {
            "key": "X-Frame-Options",
            "value": "DENY"
          },
          {
            "key": "X-XSS-Protection",
            "value": "1; mode=block"
          }
        ]
      }
    ],
    "rewrites": [
      {
        "source": "/api/**",
        "function": "api"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}
```

## Common mistakes

- **Setting the public directory to the source folder instead of the build output (e.g., src instead of dist)** — undefined Fix: 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.
- **Forgetting to build before deploying, which uploads stale or missing files** — undefined Fix: Run your build command (npm run build) before every firebase deploy. Consider adding a predeploy script in firebase.json to automate this.
- **Caching index.html aggressively, which prevents users from seeing updated deployments** — undefined Fix: Set Cache-Control: no-cache on index.html so browsers always check for updates. Cache static assets with hashed filenames aggressively instead.
- **Placing the SPA rewrite before API function rewrites, which prevents Cloud Functions from receiving requests** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-host-spa-with-firebase-hosting
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-host-spa-with-firebase-hosting
