Six-role admin system for a multi-sided marketplace
A marketplace with dealers, rental companies, buyers, and internal staff needs an admin system that does more than toggle records on and off. The client had six distinct roles in the organization — super admins, regular admins, accountants, managers, dealers, and rental operators — each needing different views, different permissions, and different workflows. The challenge wasn't building a dashboard. It was building a permission architecture that could enforce boundaries between these roles without turning the codebase into a spaghetti of conditionals.
rbac in middleware
The RBAC system is enforced at the Next.js middleware level, before any page or API route renders. When a request arrives, middleware reads the JWT, extracts the user's role, and checks it against a permission map that defines which roles can access which subdomain sections. But role alone isn't enough — within the admin subdomain, a MANAGER can see lead analytics and CRM tools but can't touch billing. An ACCOUNTANT can manage invoices and balances but can't moderate listings. So the permission map is two-dimensional: role plus section. The middleware checks both, and if either doesn't match, the request gets a clean redirect instead of an error page. This keeps permission logic out of individual page components entirely — by the time a component renders, access has already been validated.
moderation and audit trail
Content moderation is the daily heartbeat of the admin system. Every listing submitted by a dealer or rental company enters a moderation queue with a ModerationStatus: pending, approved, rejected, or revision_requested. Moderators (admins and managers) review listings, flag issues, and either approve or send back with specific feedback. Every moderation action creates a ModerationLog entry — who reviewed it, when, what status they assigned, and an optional comment. This audit trail is not just for accountability; it's the primary tool for resolving disputes when a dealer contests a rejection.
scaling admin writes
The audit logging goes beyond moderation. Every significant admin action writes to a structured log: price changes for lead classes, plan modifications, user role changes, global setting updates, manual balance adjustments. The log captures the actor (who), the action (what), the target (which entity), the before-state, and the after-state. This turned out to be more expensive than expected in terms of database writes — on a busy day, the moderation queue alone generates hundreds of log entries. We optimized by batching log writes where possible and keeping the log table lean (storing only diffs, not full entity snapshots) while maintaining enough detail for any audit question the client might ask.
settings without deploys
Global settings management gives super admins control over platform economics without deploying code. Lead prices for each class (A, B, C), listing TTL (how long a listing stays active before requiring renewal), featured placement pricing, and other business parameters live in a settings table and are cached at the application level. When an admin changes a setting, the cache invalidates and the new value takes effect immediately across all subdomains. This seems minor, but it was critical for the client's ability to experiment with pricing — they adjusted lead prices four times in the first two months based on dealer feedback and conversion data.
workflows, partners, and lessons
The manager and accountant workflows overlap in one uncomfortable area: billing. Managers deal with dealers daily and often need to see a dealer's billing status to understand why leads aren't being delivered. Accountants manage the actual invoices and balance adjustments. Without clear boundaries, this overlap turns into conflicts — a manager promises a billing adjustment that the accountant hasn't approved, or an accountant pauses a dealer's account that a manager is actively onboarding. We solved this with read-vs-write permissions: managers can view billing data but not modify it, and any billing exception requires a formal request that the accountant processes. It's a workflow constraint, not a technical one, but enforcing it in the UI prevents most of the friction.
The partner requests pipeline handles incoming applications from new dealers and rental companies. Applications come through a form, enter a review queue, get assigned to a manager, go through verification (business documents, inventory checks), and either get approved with an account created or rejected with reasoning. The pipeline tracks every stage with timestamps, so the team can measure how long onboarding takes and where bottlenecks form. Recharts powers the analytics views — conversion funnels, moderation throughput, revenue by dealer tier, lead volume trends — giving the internal team the same quality of data visibility that the dealers get in their own portals.
The honest lesson: building for six roles sounds like it's six times the work, but it's actually worse — it's the combinatorial explosion of six roles interacting with dozens of sections and each other. The middleware-level permission enforcement was the single best architectural decision. It meant we never had to think about access control inside a component — if you're rendering, you're authorized. The audit log, which we almost deprioritized as "nice to have," turned out to be essential for the client within the first week of operation, when a disputed moderation decision needed to be traced step by step.
Stack
Frontend: Next.js 15, React 19, Tailwind CSS, TanStack Query, Recharts
Auth & routing: jose (JWT), Next.js middleware (role + section RBAC)
Backend: Next.js Route Handlers, Prisma 6, PostgreSQL
Audit: Structured logging with actor/action/target/diff pattern
