npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

firebase-multi-env

v1.5.0

Published

Multi-environment Firebase routing (Origin + allowedEnvs) with pinned per-env deploys and project-parity isolation

Readme

firebase-multi-env

npm

Hardened single-project environment isolation for Firebase: Origin → environment → Firestore database, with claim-based authorization for gated environments and pinned per-env deploys as the production path.

One Firebase project, multiple Firestore databases, multiple Hosting sites. Production users do not need special rights; gated envs (qual/cert/…) require an allowedEnvs claim.

Production path: pinned mode + per-env service accounts + secrets + deploy isolation (project-parity). Separate Firebase projects remain the strongest blast-radius boundary for billing/Auth/admin — see Security model and templates/PROJECT_PARITY.md.

Package layout

src/           # runtime, server, functions, client
eslint/        # no-bare-admin-firestore, require-pinned-runtime
templates/     # rules, IAM, secrets, deploy isolation, project parity
bin/           # grant-env, init, doctor [--strict], provision

Public imports:

| Import | Purpose | |---|---| | firebase-multi-env/server | createEnvRuntime, createGetDb, createGetDbForEnv, guards | | firebase-multi-env/functions-v1 | callable wrapper (v1) | | firebase-multi-env/functions-v2 | callable wrapper (v2) | | firebase-multi-env/http | onRequest / Express-style wrapper | | firebase-multi-env/client | callable (+ prefix) + client Firestore kit | | firebase-multi-env/eslint | ESLint plugin (forbid bare Admin Firestore / require pinned) |

Install

npm install firebase-multi-env
npm install firebase firebase-admin firebase-functions

Local link:

npm run build && npm link
# in your app
npm link firebase-multi-env

Security model

| Request from | Database | Needs allowedEnvs? | |---|---|---| | Public env Hosting origin (e.g. prod) | that env's DB | No | | Gated env Hosting origin (e.g. qual/cert) | that env's DB | Yes | | Localhost → cloud gated env | gated DB | Yes | | Full local emulators | emulator default DB | No (optional) |

Client appEnv is only a hint on localhost. Hosted Origin always wins when recognized.

This is request routing plus pinned deploy isolation. Pair with IAM, secrets, and CI for project-parity. It is not a cryptographic Hosting lock.

Protects against: client appEnv overrides, accidental dynamic DB selection, ungated non-prod access, wrong Origin on a pinned runtime, unpinned Cloud deploys (startup assert), silent getDb() outside request context (when configured), leaked emulator env vars on pinned deploys, bare Admin Firestore (ESLint + doctor).

Does not automatically protect against: overly broad service accounts, shared secrets, bad IAM/CI, or shared-project Auth/billing blast radius — close those with the templates under multi-env/ after init.

Full matrix: SECURITY.md and templates/THREAT_MODEL.md.

Isolation (pinned) — required for production

| pinned | Deploy shape | What Origin does | |---|---|---| | false (default) | One process may serve many envs | Selects which DB (local/dev only) | | true | One deploy (+ SA) per env | Confirms the pinned env |

Deployed Cloud Functions reject unpinned config at startup unless allowUnpinnedCloudDeploy: true.

export const appEnvRuntime = createEnvRuntime({
  pinned: true,
  pinnedEnvironment: process.env.APP_ENV, // "qual" for the qual deploy
  environments: { /* ... */ },
  onResolveEnv: (event) => logger.info('env_resolved', event), // optional audit
});

Pinned defaults:

  • Unknown / missing hosted Origin → reject (rejectUnknownOrigin: true)
  • getDb() / getRuntimeEnv() outside a request wrapper → throw
  • Referer fallback → off (allowRefererFallback: false)
  • Emulator host env vars on a real deploy → refuse
  • Runtime refuses to serve any env other than the pinned one

Project-parity stack (via init):

  • multi-env/PROJECT_PARITY.md
  • multi-env/iam-sa-per-env.md
  • multi-env/secrets-per-env.md
  • multi-env/deploy-isolation.md
  • multi-env/github-actions.deploy.example.yml
  • Storage + Firestore rules snippets

Scripts and background jobs should use an explicit DB accessor (never rely on silent defaults):

import { createGetDb, createGetDbForEnv } from 'firebase-multi-env/server';

export const getDb = createGetDb(appEnvRuntime);
export const getDbForEnv = createGetDbForEnv(appEnvRuntime);

// in a scheduled job / script:
const db = getDbForEnv('qual');

Optional local hardening without pinning (not for Cloud deploys):

createEnvRuntime({
  pinned: false, // local/emulator only — blocked on Cloud unless allowUnpinnedCloudDeploy
  rejectUnknownOrigin: true,
  requireRequestContext: true,
  allowRefererFallback: false,
  environments: { /* ... */ },
});

Quick start scaffolding

npx firebase-multi-env init
npx firebase-multi-env provision --project my-app --envs production,qual
npx firebase-multi-env doctor --strict

Writes:

  • firestore.rules.snippets/ (Firestore + Storage, gated + public)
  • MULTI_ENV_SETUP.md
  • multi-env/ — project parity, IAM, secrets, deploy isolation, pinned examples, CI workflow
  • multi-env/provision/ — generated gcloud scripts (via provision)

ESLint guardrails

import multiEnv from 'firebase-multi-env/eslint';

export default [
  {
    plugins: { 'firebase-multi-env': multiEnv },
    rules: {
      'firebase-multi-env/no-bare-admin-firestore': 'error',
      'firebase-multi-env/require-pinned-runtime': 'error',
    },
  },
];

One-time Firebase setup

firebase firestore:databases:create qual-env --location nam5
firebase firestore:databases:create cert-env --location nam5
firebase hosting:sites:create myapp-qual
firebase hosting:sites:create myapp-cert
firebase target:apply hosting qual myapp-qual
firebase target:apply hosting cert myapp-cert
firebase target:apply hosting prod myapp

Example firebase.json (same Functions source, unique prefix per env so IDs do not collide):

{
  "functions": [
    { "source": "functions", "codebase": "prod", "prefix": "prod", "configDir": "functions/config/prod" },
    { "source": "functions", "codebase": "qual", "prefix": "qual", "configDir": "functions/config/qual" },
    { "source": "functions", "codebase": "cert", "prefix": "cert", "configDir": "functions/config/cert" }
  ],
  "firestore": [
    { "database": "(default)", "rules": "firestore.prod.rules", "indexes": "firestore.indexes.json" },
    { "database": "qual-env", "rules": "firestore.qual.rules", "indexes": "firestore.indexes.json" },
    { "database": "cert-env", "rules": "firestore.cert.rules", "indexes": "firestore.indexes.json" }
  ],
  "hosting": [
    {
      "target": "qual",
      "public": "dist",
      "rewrites": [{ "source": "/api/**", "function": { "functionId": "qual-api", "codebase": "qual" } }]
    },
    {
      "target": "cert",
      "public": "dist",
      "rewrites": [{ "source": "/api/**", "function": { "functionId": "cert-api", "codebase": "cert" } }]
    },
    {
      "target": "prod",
      "public": "dist",
      "rewrites": [{ "source": "/api/**", "function": { "functionId": "prod-api", "codebase": "prod" } }]
    }
  ]
}

Full example: templates/firebase.codebases.example.json (copied under multi-env/ by init). Rules templates ship in templates/ (and via init). Gated DBs must check allowedEnvs; prod should not.

Cloud Functions (callables)

import {
  createEnvRuntime,
  createGetDb,
  createGetDbForEnv,
  requireAuth,
  requireOwner,
} from 'firebase-multi-env/server';
import { createWithAppEnvV1 } from 'firebase-multi-env/functions-v1';

export const appEnvRuntime = createEnvRuntime({
  pinned: true,
  pinnedEnvironment: process.env.APP_ENV,
  environments: {
    production: {
      database: '(default)',
      origins: ['https://myapp.web.app', 'https://myapp.firebaseapp.com'],
    },
    qual: {
      database: 'qual-env',
      origins: ['https://myapp-qual.web.app'],
      requireClaim: true,
    },
    cert: {
      database: 'cert-env',
      origins: ['https://myapp-cert.web.app'],
      requireClaim: true,
    },
  },
});

export const getDb = createGetDb(appEnvRuntime);
export const getDbForEnv = createGetDbForEnv(appEnvRuntime);
export const withAppEnv = createWithAppEnvV1(appEnvRuntime);

export const syncData = functions.https.onCall(withAppEnv(async (data, context) => {
  const auth = requireAuth(context.auth);
  requireOwner(auth, data.userId);
  const db = getDb();
  // ...
}));

Optional overrides: HOST_ORIGINS_<ENV> (comma-separated).

HTTP functions

import { createWithAppEnvHttp } from 'firebase-multi-env/http';
import { onRequest } from 'firebase-functions/v2/https';

// verifyIdToken is recommended: onRequest does not populate req.auth by default.
const withHttp = createWithAppEnvHttp(appEnvRuntime, { verifyIdToken: true });

export const api = onRequest(withHttp(async (req, res) => {
  const db = getDb();
  res.json({ env: appEnvRuntime.getEnvTag() });
}));

Localhost hints: x-app-env header or ?appEnv=.

Web client

import { createMultiEnvClient } from 'firebase-multi-env/client';
import { getFunctions } from 'firebase/functions';

const appEnv = import.meta.env.VITE_APP_ENV;
const { callable, getDb } = createMultiEnvClient({
  app,
  functions: getFunctions(app),
  appEnv,
  // Must match firebase.json functions[].prefix (CLI deploys `${prefix}-${name}`)
  prefixes: {
    production: 'prod',
    qual: 'qual',
    cert: 'cert',
  },
  databases: {
    production: '(default)',
    qual: 'qual-env',
    cert: 'cert-env',
  },
});

await callable('syncData')({ /* payload */ }); // → qual-syncData when appEnv is "qual"
const db = getDb();

Or use createCallable / resolveFunctionId / createGetClientFirestore individually.

Grant environment access

Auth is shared in one Firebase project. Non-prod access uses allowedEnvs claims:

gcloud auth application-default login
npx firebase-multi-env grant-env qual --project my-project [email protected]
npx firebase-multi-env grant-env cert --project my-project [email protected]
# → { allowedEnvs: ['qual', 'cert'] }
# sign out / sign in

npx firebase-multi-env grant-env qual --revoke --project my-project [email protected]

Provision per-env IAM (scripts only)

Generate reviewable gcloud scripts for runtime SAs, Storage buckets, and secret accessors:

npx firebase-multi-env provision --project my-project --envs production,qual,cert
bash multi-env/provision/provision.all.sh

See templates/PROVISION.md. Scripts do not call GCP until you run them.

What this package covers

  • Origin → environment → Firestore database routing
  • Pinned production path (unpinned blocked on Cloud deploys by default)
  • Gated-env allowlist claims for Functions (callable + HTTP)
  • Hardened Origin parsing (no multi-value / null / non-http schemes)
  • Optional Referer fallback (off by default when pinned)
  • Emulator-env leak refusal on deployed pinned functions
  • getDbForEnv for scripts/jobs; fail-closed getDb when request context is required
  • onResolveEnv audit hook
  • Optional HTTP ID token verification
  • Client callable + Firestore helpers (prefixes / resolveFunctionId for firebase.json prefix)
  • Server guards (requireAuth, requireOwner, requireClaim)
  • Rules templates (Firestore + Storage) + init / doctor --strict
  • ESLint plugin (no-bare-admin-firestore, require-pinned-runtime)
  • CLI grant/revoke for allowedEnvs (shared Auth + claims)
  • CLI provision — generate per-env SA / bucket / secret gcloud scripts
  • Templates for per-env SAs, secrets, deploy isolation, project-parity checklist

Still app-owned: Auth UI/sign-in flows, domain-specific RBAC, live IAM bindings, org policies, and full product security rules beyond the templates.

Releasing

Releases use semantic-release on main.

  1. npm granular token (read/write + bypass 2FA)
  2. GitHub Actions secret NPM_SECRET (mapped to NPM_TOKEN in the workflow)
  3. Conventional Commits: fix: patch, feat: minor, BREAKING CHANGE / feat!: major

License

MIT