← Back to @payez/next-mvp
RBAC

Role-Based Access Control

Middleware-enforced roles on every route. System roles + custom roles.

No manual auth checks scattered through your code. Roles are declared, enforced by middleware, and visible in the admin dashboard. You define who can access what — the framework enforces it.

How it works

1

Roles are defined in the vibe_app.roles table — some are system roles (immutable, like vibe_app_admin), others are custom roles you create for your app.

2

Users are assigned roles via the vibe_app.user_roles table (or the admin dashboard). Role assignments include who assigned them and when — for audit purposes.

3

Every request passes through middleware that reads the user's roles from their JWT/session. If the route requires a role the user doesn't have, they get a 403.

4

The page_permissions table maps route patterns to required roles. You can also require 2FA for specific routes. This is declarative — no if-checks in your handlers.

System roles vs. custom roles

System roles are built into the platform and can't be deleted or renamed: - vibe_app_system_admin — full platform access, can manage other admins - vibe_app_admin — application admin, can manage users and settings - custom_schemas_admin — can create and modify custom schema tables Custom roles are yours. Create "editor", "billing_manager", "support_agent" — whatever your app needs. They're stored in vibe_app.roles and work identically to system roles in middleware checks.

// Role definitions in vibe_app.roles
// System roles (is_system = true, immutable)
vibe_app_system_admin
vibe_app_admin
custom_schemas_admin

// Your custom roles (is_system = false)
editor
billing_manager
support_agent
viewer

Route-level permissions

The page_permissions table lets you declare which roles can access which routes — and whether 2FA is required. This is checked by middleware before your handler runs. You never write: if (!user.roles.includes("admin")) return 403. The middleware does it. Your handler only runs if the user is authorized.

-- page_permissions table
INSERT INTO vibe_app.page_permissions
  (route_pattern, required_roles, requires_2fa)
VALUES
  ('/admin/*',    '{"vibe_app_admin"}',    true),
  ('/billing/*',  '{"billing_manager"}',   true),
  ('/editor/*',   '{"editor"}',            false),
  ('/api/admin/*','{"vibe_app_admin"}',     false);

-- Wildcards match nested routes
-- Multiple roles = user needs ANY of them (OR logic)

Role-aware UI components

The MVP includes role-aware navigation components. The admin sidebar only appears if the user has admin roles. Tab bars show/hide items based on roles. The avatar dropdown menu adapts. This isn't just hiding elements — the middleware enforces it server-side. Even if someone manually navigates to /admin, the middleware blocks the request before the page renders.

Helper functions

The admin-roles.ts module exports typed helpers for role checking in your server-side code. These are useful for fine-grained authorization within handlers — when you need more than route-level checks.

import {
  SYSTEM_ADMIN_ROLES,
  SCHEMAS_ADMIN_ROLES,
  ALL_ADMIN_ROLES,
  hasSystemAdminRole,
  hasSchemasAdminRole,
} from '@/lib/admin-roles';

// In an API route handler
export async function POST(req: Request) {
  const session = await getSession();

  if (!hasSystemAdminRole(session.roles)) {
    return Response.json(
      { error: 'System admin required' },
      { status: 403 }
    );
  }

  // Only system admins reach this point
}

Audit trail for role changes

Every role assignment and revocation is logged in vibe_app.audit_log with: who made the change, what role was added/removed, which user was affected, and a timestamp. This means you can answer: "Who gave Jane admin access?" and "When was the editor role revoked from user 42?" — without any custom logging code.

This is included. Not extra.

Role-Based Access Control ships with every @payez/next-mvp install. 10K MAU free. No credit card required.