Yup — you can hand Replit the exact prompts/code below and it’ll wire this up. And we’ll add Analyst now.

0) The roles we’ll support (final)

owner (you)

management

staff

analyst (read-only)

end-user plans: user, pro (unchanged)

1) Update the admin-claim script (so you can set any role)

Prompt to Replit Agent:

Create or update tools/setAdminClaim.ts to accept uid and role. It should set a Firebase custom claim { role }. Use Admin SDK creds from env vars FIREBASE_PROJECT_ID, FIREBASE_CLIENT_EMAIL, FIREBASE_PRIVATE_KEY (replace \n in the key). Validate role ∈ ['owner','management','staff','analyst','user','pro']. Print the assigned role.

Code (full file):

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

const required = ["FIREBASE_PROJECT_ID","FIREBASE_CLIENT_EMAIL","FIREBASE_PRIVATE_KEY"] as const;
for (const k of required) {
  if (!process.env[k]) throw new Error(`Missing env: ${k}`);
}

if (!admin.apps.length) {
  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),
  });
}

const validRoles = new Set(["owner","management","staff","analyst","user","pro"]);

async function main() {
  const [uid, role] = process.argv.slice(2);
  if (!uid || !role || !validRoles.has(role)) {
    console.error("Usage: tsx tools/setAdminClaim.ts <uid> <role>");
    console.error("Roles:", [...validRoles].join(", "));
    process.exit(1);
  }
  await admin.auth().setCustomUserClaims(uid, { role });
  const user = await admin.auth().getUser(uid);
  console.log(`✅ Set role=${role} for ${user.email ?? uid}`);
}
main().catch(e => { console.error(e); process.exit(1); });


Run (examples):

# make yourself owner
tsx tools/setAdminClaim.ts YOUR_UID owner
# add a read-only analyst
tsx tools/setAdminClaim.ts SOMEONE_ELSE_UID analyst


Then sign out/in so the new claim is in the ID token.

2) Backend guard (Express middleware)

Prompt:

Add an authz helper with role checks. Owner > Management > Staff > Analyst. Export guards for: requireOwner, requireManagementOrAbove, requireStaffOrAbove, requireAnalystOrAbove.

Code:

// server/middleware/authz.ts
import { Request, Response, NextFunction } from "express";

type Role = "owner"|"management"|"staff"|"analyst"|"user"|"pro" | undefined;
const rank: Record<Exclude<Role, undefined>, number> = {
  owner: 4, management: 3, staff: 2, analyst: 1, pro: 0, user: 0
};

function hasRole(userRole: Role, min: Role) {
  if (!userRole || !min) return false;
  return (rank[userRole as keyof typeof rank] ?? -1) >= (rank[min as keyof typeof rank] ?? 999);
}

function guard(minRole: Role) {
  return (req: Request, res: Response, next: NextFunction) => {
    const role = (req as any).user?.role as Role; // set by your auth decode middleware
    if (hasRole(role, minRole)) return next();
    return res.status(403).json({ error: "Forbidden" });
  };
}

export const requireAnalystOrAbove = guard("analyst");
export const requireStaffOrAbove   = guard("staff");
export const requireManagementOrAbove = guard("management");
export const requireOwner          = guard("owner");


Use in routes:

import { requireOwner, requireManagementOrAbove } from "./middleware/authz";

app.get("/admin/users", requireManagementOrAbove, listUsers);
app.post("/admin/settings", requireOwner, updateSettings);

3) Firestore Security Rules (prevent self-promotion)

Prompt:

Update firestore.rules to block users from changing their own role field. Admins with custom claim role ∈ {owner,management} may write roles; others cannot.

Rules:

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function isSignedIn() { return request.auth != null; }
    function role() { return request.auth.token.role; }
    function isMgmt() { return role() == 'owner' || role() == 'management'; }

    match /users/{uid} {
      allow read: if isSignedIn() && request.auth.uid == uid;
      allow update: if isSignedIn() && request.auth.uid == uid
                    && !('role' in request.resource.data); // users can't change role

      // Admins may manage roles:
      allow update: if isMgmt();
      allow list, get: if isMgmt(); // adjust as needed
    }

    // lock down admin collections as needed...
  }
}

4) React UI gating (show/hide admin features)

Prompt:

Add a useRole() hook that reads auth.currentUser custom claim role and re-checks after login (force token refresh). Use it to gate Admin links and sections.

Code:

// src/hooks/useRole.ts
import { useEffect, useState } from "react";
import { getAuth } from "firebase/auth";

export function useRole() {
  const [role, setRole] = useState<string | null>(null);
  useEffect(() => {
    const auth = getAuth();
    const unsub = auth.onIdTokenChanged(async (u) => {
      if (!u) return setRole(null);
      await u.getIdToken(true); // refresh to get latest claims
      const token = await u.getIdTokenResult();
      setRole((token.claims.role as string) || null);
    });
    return () => unsub();
  }, []);
  return role;
}


Use it:

import { useRole } from "@/hooks/useRole";

function AdminNav() {
  const role = useRole();
  return (
    <>
      {(role === "owner" || role === "management") && <a href="/admin/users">Users</a>}
      {(role === "owner") && <a href="/admin/settings">System Settings</a>}
      {(role === "analyst" || role === "staff" || role === "management" || role === "owner") && <a href="/admin/reports">Reports</a>}
    </>
  );
}

5) Admin Dashboard counts (Free vs Pro)

Prompt:

In the admin API, add two counts: freeUsers where plan == 'free' and proUsers where plan == 'pro'. Return them in the dashboard payload and render two new StatCards.

Backend (Firestore queries):

const free = await db.collection("users").where("plan","==","free").count().get();
const pro  = await db.collection("users").where("plan","==","pro").count().get();
res.json({ totalUsers, freeUsers: free.data().count, proUsers: pro.data().count, ... });


Frontend:

<StatCard label="Total Users" value={totalUsers} />
<StatCard label="Free Users" value={freeUsers} />
<StatCard label="Pro Users" value={proUsers} />

6) Seeding roles (one-time)

Make you owner: tsx tools/setAdminClaim.ts YOUR_UID owner

Make a trusted person management or analyst as needed.

Keep staff minimal (support team) with no billing/system settings.