Nuxt + Firebase security — Nitro server + Firestore rules

Nuxt 3's Nitro server combined with Firebase gives you a capable Vue-based full-stack. The security model is two-layer: Firestore/Storage Security Rules enforce access at the database, and Nitro /server/api endpoints handle anything that can't be expressed in rules (third-party API calls, complex workflows, admin operations). The two failure modes specific to this stack: Firestore rules misconfigurations (same as any Firebase app — default-allow, missing ownership checks), and the Nuxt `runtimeConfig` public/private split. `runtimeConfig.public.*` is serialized into every page; anything you put there ships to the browser. Secrets must live under `runtimeConfig.*` (private) and be accessed only from /server/api routes. App Check matters here too. Without it, attackers can call your Firebase project using your public web config scraped from any visit to your Nuxt site.

What breaks on this stack

Default-allow Firestore rules

Same as any Firebase app. /guides/firebase-rules-security has the full pattern.

Unauthenticated /server/api routes

Nitro endpoints need auth middleware. Without it, POST to /server/api/something.post.ts bypasses client-side guards.

runtimeConfig public leak

Anything under `runtimeConfig.public` is serialized into __NUXT__ on every page. Secrets there are effectively public. Move to unprefixed `runtimeConfig`.

Admin SDK in client path

firebase-admin must be used only inside /server/api/ routes, never imported into components or pages. A stray import triggers Admin SDK loading at build-time.

App Check not enabled

Without App Check, your Firebase project accepts requests from any script with your public config.

Pre-ship checklist

  • Firestore rules scoped by auth.uid + tenant, tested with Emulator Suite
  • storage.rules mirror firestore.rules patterns
  • Every /server/api route has auth middleware or per-route check
  • runtimeConfig public / private split audited on every change
  • firebase-admin imported only from /server/ paths
  • App Check enabled in production with reCAPTCHA v3 or Enterprise
  • Callable Cloud Functions have context.auth check
  • Security headers set in /server/middleware/security.ts
  • Anonymous auth disabled unless explicitly needed
  • Cloud Functions use least-privilege IAM roles

Starter config

// nuxt.config.ts — runtimeConfig split
export default defineNuxtConfig({
  runtimeConfig: {
    // server-only (not shipped to client)
    firebaseAdminKey: process.env.FIREBASE_ADMIN_KEY,
    stripeSecretKey: process.env.STRIPE_SECRET_KEY,
    public: {
      // serialized into __NUXT__ — public-safe only
      firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY,
      firebaseProjectId: process.env.NUXT_PUBLIC_FIREBASE_PROJECT_ID,
    },
  },
});

// server/middleware/auth.ts — auth check on /server/api/*
import { getAuth } from "firebase-admin/auth";
export default defineEventHandler(async (event) => {
  if (!event.node.req.url?.startsWith("/api/")) return;
  const token = getHeader(event, "authorization")?.replace("Bearer ", "");
  if (!token) throw createError({ statusCode: 401 });
  try {
    event.context.user = await getAuth().verifyIdToken(token);
  } catch {
    throw createError({ statusCode: 401, message: "invalid token" });
  }
});

// server/api/orders/[id].get.ts — protected endpoint
export default defineEventHandler(async (event) => {
  const user = event.context.user;
  if (!user) throw createError({ statusCode: 401 });
  const id = getRouterParam(event, "id");
  // Firestore admin query with user + tenant scoping
});