2) Firebase Staff Admin (separate from customer auth)

Goal: staff log in via Firebase (Google or email/pass), get an admin:true custom claim, then access /admin/* APIs. Customers still use your JWT/SQLite auth—no mixing.

Install
npm i firebase-admin firebase

Server: Firebase Admin middleware

Create a small verifier; protect admin routes with it.

// server/admin/firebaseAdmin.ts
import * as admin from "firebase-admin";

const creds = {
  projectId: process.env.FIREBASE_PROJECT_ID!,
  clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
  privateKey: (process.env.FIREBASE_PRIVATE_KEY || "").replace(/\\n/g, "\n"),
};

if (!admin.apps.length) {
  admin.initializeApp({ credential: admin.credential.cert(creds as any) });
}

export async function verifyAdminBearer(authHeader?: string) {
  if (!authHeader?.startsWith("Bearer ")) throw new Error("missing");
  const token = authHeader.slice(7);
  const decoded = await admin.auth().verifyIdToken(token, true);
  if (!decoded.admin) throw new Error("not_admin"); // requires custom claim
  return decoded; // contains uid, email, etc.
}


Admin guard + sample route:

// server/admin/routes.ts
import { Router } from "express";
import { verifyAdminBearer } from "./firebaseAdmin";

const router = Router();

router.get("/api/admin/health", async (req, res) => {
  try {
    await verifyAdminBearer(req.headers.authorization);
    res.json({ ok: true, ts: Date.now() });
  } catch {
    res.status(403).json({ error: "Forbidden" });
  }
});

// Example protected op
router.get("/api/admin/users/count", async (req, res) => {
  try {
    await verifyAdminBearer(req.headers.authorization);
    // return aggregate from SQLite
    const row = require("../db").db.prepare("SELECT COUNT(*) as c FROM users").get();
    res.json({ users: row.c });
  } catch {
    res.status(403).json({ error: "Forbidden" });
  }
});

export default router;


Wire routes:

// server/index.ts
import adminRoutes from "./admin/routes";
app.use(adminRoutes);

Give staff the admin claim (one-time per staffer)

Run this small script locally (with your Admin creds env set):

// tools/setAdminClaim.ts
import * as admin from "firebase-admin";

admin.initializeApp({
  credential: admin.credential.cert({
    projectId: process.env.FIREBASE_PROJECT_ID!,
    clientEmail: process.env.FIREBASE_CLIENT_EMAIL!,
    privateKey: (process.env.FIREBASE_PRIVATE_KEY || "").replace(/\\n/g, "\n"),
  } as any),
});

async function setAdmin(uid: string, adminEmail?: string) {
  await admin.auth().setCustomUserClaims(uid, { admin: true, email: adminEmail });
  console.log("Admin claim set for:", uid);
}
setAdmin(process.argv[2], process.argv[3]).catch(console.error);


Usage:

FIREBASE_PROJECT_ID=xxx \
FIREBASE_CLIENT_EMAIL=yyy \
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" \
node tools/setAdminClaim.js <firebase-uid> <email@yourcompany.com>

Frontend: Staff admin login (Firebase Web SDK)

Add Firebase config envs:

VITE_FB_API_KEY=...
VITE_FB_AUTH_DOMAIN=...
VITE_FB_PROJECT_ID=...


Init + login helper:

// src/admin/firebaseClient.ts
import { initializeApp } from "firebase/app";
import { getAuth, signInWithPopup, GoogleAuthProvider, getIdTokenResult } from "firebase/auth";

const app = initializeApp({
  apiKey: import.meta.env.VITE_FB_API_KEY,
  authDomain: import.meta.env.VITE_FB_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FB_PROJECT_ID,
});
export const auth = getAuth(app);

export async function signInAdminWithGoogle(): Promise<string> {
  const provider = new GoogleAuthProvider();
  const cred = await signInWithPopup(auth, provider);
  const t = await cred.user.getIdToken(true);
  const claims = (await getIdTokenResult(cred.user)).claims as any;
  if (!claims.admin) throw new Error("Not an admin account");
  return t; // Firebase ID token with admin claim
}


Admin page:

// src/pages/Admin/AdminLogin.tsx
import { useState } from "react";
import { signInAdminWithGoogle } from "@/admin/firebaseClient";

export default function AdminLogin() {
  const [err, setErr] = useState<string | null>(null);

  const handleSignIn = async () => {
    try {
      const token = await signInAdminWithGoogle();
      sessionStorage.setItem("adminToken", token);
      window.location.href = "/admin/dashboard";
    } catch (e: any) {
      setErr(e.message || "Sign-in failed");
    }
  };

  return (
    <div className="max-w-md mx-auto py-12">
      <h1 className="text-2xl font-bold mb-2">Staff Admin</h1>
      <p className="text-sm text-gray-600 mb-6">Company-only access</p>
      <button className="rounded-xl bg-black text-white px-4 py-2" onClick={handleSignIn}>
        Sign in with Google (Staff)
      </button>
      {err && <div className="mt-4 text-red-600 text-sm">{err}</div>}
    </div>
  );
}


Calling a protected admin API:

// src/pages/Admin/Dashboard.tsx
import { useEffect, useState } from "react";

export default function Dashboard() {
  const [stats, setStats] = useState<any>(null);
  useEffect(() => {
    const token = sessionStorage.getItem("adminToken");
    if (!token) { window.location.href = "/admin/login"; return; }
    fetch("/api/admin/users/count", { headers: { Authorization: `Bearer ${token}` } })
      .then(r => r.json()).then(setStats).catch(()=>{});
  }, []);
  return (
    <div className="max-w-2xl mx-auto py-10">
      <h1 className="text-2xl font-bold mb-4">Admin Dashboard</h1>
      <pre className="text-sm bg-gray-50 p-4 rounded-xl">{JSON.stringify(stats, null, 2)}</pre>
    </div>
  );
}


Notes

Staff authentication is totally separate: customers use your JWT; staff uses Firebase tokens with the admin claim.

Admin envs needed on the server (Admin SDK) and client (Web SDK).

Keep the admin UI at /admin/* and never reuse customer cookies there.