Super Admin Portal
Platform-operator-only control panel at
/admin/*for managing every tenant (stores, users, subscriptions, revenue, health) across the Eziseller install. Audience: new dev with Node/React experience, no Eziseller context.
1. Overview
Regular owners and staff operate inside a single tenant boundary (their store, scoped by ownerId). The Super Admin portal is the cross-tenant surface: it lets platform operators see every store, deactivate abusers, override subscriptions, create more admins, and monitor platform-wide health. It is mounted at /admin/* on the frontend and /api/admin/* on the backend, and is gated end-to-end by the SUPER_ADMIN role. There are exactly seven sections: Overview, Stores, Users, Subscriptions, Revenue, System Health, Settings. It deliberately does not reuse the tenant dashboard routes — admin endpoints are separate so their queries can skip ownerId scoping without risking a leak into tenant code paths.
2. Architecture
AdminGuard only hides UI — the real authorization is authenticateToken + requireSuperAdmin applied once at the router root in backend/routes/admin/index.ts:23. Every admin sub-router inherits that gate, so individual endpoints never re-check the role.
3. Data model
SUPER_ADMIN users have no ownedStore and no ownerId — they exist outside the tenant graph. All tenant scoping in the rest of the backend keys off ownerId; admin routes simply omit that filter. See schema.prisma.
4. Sections & key endpoints
4.1 Overview (/admin)
High-level dashboard. Pulls from GET /api/admin/subscriptions/overview (status breakdown, trials expiring in 7 days, count-by-plan, total stores/users).
4.2 Stores (/admin/stores)
GET /api/admin/stores— paginated list with search onname/slug, includes owner + product/order counts.GET /api/admin/stores/:id— detail with owner, latest subscription, counts.PATCH /api/admin/stores/:id/toggle-active— flipsStore.isActive. Not currently enforced anywhere — see gotchas.
4.3 Users (/admin/users)
GET /api/admin/users— paginated list, filterable byrole/isActive, search on name/email.GET /api/admin/users/:id— detail with store, latest subscription, counts.PATCH /api/admin/users/:id/toggle-active— refuses onSUPER_ADMIN.DELETE /api/admin/users/:id— hard delete, refuses onSUPER_ADMIN.POST /api/admin/users/:id/impersonate— mints access + refresh tokens for the target user; refuses onSUPER_ADMIN. Adds the refresh token to the in-memoryrefreshTokenStore.
4.4 Subscriptions (/admin/subscriptions)
GET /api/admin/subscriptions/overview— aggregates for the Overview page.GET /api/admin/subscriptions— paginated, filters:status,billingCycle,planSlug.PATCH /api/admin/subscriptions/:id/override— the god-mode endpoint. Accepts any ofplanSlug,status,billingCycle,extendDaysand writes directly toUserSubscription. Bypasses Razorpay entirely.
4.5 Revenue (/admin/revenue)
GET /api/admin/revenue— total/this-month/last-month revenue fromSubscriptionPayment(status=success), MRR (monthly plans + yearly/12), trial-to-paid conversion %, 30-day churn %, active count by plan. Pure read, ~11 parallel Prisma queries.
4.6 System Health (/admin/health)
GET /api/admin/health— draft orders with parse errors (raw SQL onparseErrorsarray), expired/rejected counts, subs expiring/expired, 7-day user+order velocity, notification queue breakdown + 10 most-recent failures.
4.7 Settings (/admin/settings)
PUT /api/admin/settings/change-password— changes the calling admin's own password.POST /api/admin/settings/create-admin— creates anotherSUPER_ADMIN(no store, no subscription).GET /api/admin/settings/admins— lists all super admins.
4.8 Jobs & cron (also mounted on admin root)
POST /api/admin/jobs/update-expired-subscriptions— manually triggers the expired-trial job.GET /api/admin/cron/status,POST /api/admin/cron/restart— manages the subscription cron scheduler.
5. Key files
- backend/routes/admin/index.ts:23 — applies
authenticateToken + requireSuperAdminonce; mounts all sub-routers. - backend/routes/admin/stores.ts — store list/detail/toggle-active.
- backend/routes/admin/users.ts:343 — impersonation token mint.
- backend/routes/admin/subscriptions.ts:208 — subscription override.
- backend/routes/admin/revenue.ts — MRR, churn, conversion.
- backend/routes/admin/health.ts — health aggregates.
- backend/routes/admin/settings.ts — password + create-admin.
- backend/middleware/rbac.ts:48 —
requireSuperAdmin. - backend/server.ts:124 — mount point.
- src/app/admin/layout.tsx — nav +
AdminGuardwrap. - src/components/admin/AdminGuard.tsx:17 — FE role check, redirects to
/dashboardif notSUPER_ADMIN.
6. Env vars & config
| Var | Required | Purpose | What breaks |
|---|---|---|---|
JWT_SECRET | yes | Signs admin access tokens (incl. impersonation tokens) | Admin login + impersonation fail |
DATABASE_URL | yes | All admin reads/writes | Entire portal 500s |
FRONTEND_URL | yes | CORS for /admin/* fetches | Browser CORS errors from admin UI |
There is no separate admin env var — role lives in the DB on User.role. Seeding the first SUPER_ADMIN is done manually (SQL or a seed script), then create-admin bootstraps the rest.
7. Gotchas & troubleshooting
Store.isActive=falsedoes nothing yet. The stores toggle-active endpoint flips the flag, but no tenant-side middleware checks it — deactivated stores still serve requests. Enforcement is deferred.- SubscriptionGate does not block impersonation. When a super admin impersonates a tenant, the minted token carries the target user's role but
SubscriptionGateis not re-entered with the admin context, so expired-subscription users can still be impersonated into a dashboard that is normally gated. Deferred. - Admin queries bypass
ownerIdscoping by design. Every other backend route filters byreq.user.ownerId || req.user.userId. Admin routes do not. Copy-pasting a snippet fromroutes/orders.tsintoroutes/admin/*is fine; copy-pasting the other direction will leak cross-tenant data. Keep admin-scoped logic inroutes/admin/only. - Impersonation refresh tokens live in an in-memory
Set.refreshTokenStoreis reset on backend restart (seeroutes/auth). Impersonation sessions survive a refresh window only as long as the process stays up. - Health endpoint must stay fast.
/api/admin/healthfans out ~10 queries including a raw SQL ondraft_orders.parseErrors. Do not add unboundedfindManycalls withouttake:— this page is polled and a slow query will hang the whole admin shell. AdminGuardis cosmetic only. It just hides UI and redirects. Never rely on it for authorization — a user hitting/api/admin/*directly is stopped byrequireSuperAdmin, not by the React guard.create-admindoes not send an email or require email verification. The newSUPER_ADMINcan log in immediately with the password you set. Treat it like a privileged bootstrap operation.- Revenue numbers depend on
SubscriptionPayment.status='success'. Failed/pending payments are excluded. If Razorpay webhook handling is broken, revenue will under-report silently.
8. Extension points
- New admin section: add
backend/routes/admin/<name>.ts, mount it inbackend/routes/admin/index.ts, add a nav entry insrc/app/admin/layout.tsx, and createsrc/app/admin/<name>/page.tsx. The rootauthenticateToken + requireSuperAdminapplies automatically — do not re-add it. - Audit log: there is currently no persisted audit trail; destructive actions only
console.log. A cross-cutting audit middleware on the admin router is the natural place to add one. - Store deactivation enforcement: add a check in
middleware/subscription.ts(or a newrequireActiveStoremiddleware) that rejects requests whenuser.ownedStore.isActive === false.
9. Related docs
- auth/rbac.md — how
UserRoleandrequireSuperAdminfit withOWNER/STAFF. - auth/authentication.md — JWT issuance, refresh-token store (used by impersonation).
- auth/subscription-gating.md — what
SubscriptionGatedoes and why admin override bypasses it. - billing/subscription-lifecycle.md — semantics of the statuses the override endpoint writes.