User Role and Permissions System (ADMIN vs USER) in prompts.chat: A Complete RBAC Implementation

The prompts.chat codebase implements a role-based access control (RBAC) system using a UserRole enum with ADMIN and USER values, storing roles in JWT tokens via NextAuth and enforcing permissions through centralized helpers and route-level guards.

The user role and permissions system in prompts.chat for access control separates privileged administrative operations from regular user interactions through a two-tier hierarchy defined in the Prisma schema and enforced across API routes. This implementation leverages NextAuth.js to propagate role claims from the database into session objects, enabling fine-grained authorization checks that protect private prompts and administrative endpoints.

Role Definition in the Database Schema

The foundation of the RBAC system resides in prisma/schema.prisma, where the UserRole enum establishes the two possible permission levels:

enum UserRole {
  ADMIN
  USER
}

This enum is applied to the User model with a safe default: role UserRole @default(USER). Every new account starts with standard permissions unless an administrator explicitly elevates the role in the database.

How Roles Flow Through Authentication

When a user authenticates, the system queries the database and embeds the role claim into the JWT token. In src/lib/auth/index.ts, the jwt callback extracts the role from the user record and attaches it to the token:

// src/lib/auth/index.ts (jwt callback)
if (user && user.email) {
  const dbUser = await db.user.findUnique({
    where: { email: user.email },
    select: { id: true, role: true, username: true, locale: true, name: true, avatar: true },
  });
  if (dbUser) {
    token.id = dbUser.id;
    token.role = dbUser.role;           // <-- role is added here
  }
}

The session callback then surfaces this value to server-side logic:

// src/lib/auth/index.ts (session callback)
if (token && session.user) {
  session.user.id = token.id as string;
  session.user.role = token.role as string;   // <-- accessible in route handlers
}

All subsequent API calls using auth() receive a session object containing session.user.role, enabling immediate authorization decisions without additional database queries.

Enforcing Permissions at the Route Level

The system employs two complementary strategies for access control: centralized helper functions for data-specific logic and inline guards for administrative namespaces.

Protecting Admin-Only Endpoints

Every route under src/app/api/admin/* implements strict role validation. For example, src/app/api/admin/users/route.ts short-circuits non-admin requests immediately:

// src/app/api/admin/users/route.ts
if (session.user.role !== "ADMIN") {
  return new NextResponse(null, { status: 403 });
}

This pattern repeats across all administrative APIs—handling users, tags, categories, and webhooks—ensuring that ADMIN status is mandatory for platform-wide management operations.

Validating Private Prompt Access

For content-specific authorization, src/lib/prompt-access.ts provides the canViewPrompt helper that implements owner-or-admin logic:

// src/lib/prompt-access.ts
export function canViewPrompt(prompt, session) {
  if (!prompt) return false;
  if (!prompt.isPrivate) return true;
  // owner or admin
  return prompt.authorId === session?.user?.id || session?.user?.role === "ADMIN";
}

When mutating existing prompts in src/app/api/prompts/[id]/route.ts, the system combines ownership checks with role elevation:

// src/app/api/prompts/[id]/route.ts
if (existing.authorId !== session.user.id && session.user.role !== "ADMIN") {
  return new NextResponse(null, { status: 403 });
}

Regular users may only modify their own content, while administrators bypass ownership restrictions entirely.

Practical Implementation Examples

Checking Admin Status in API Routes

To implement a new admin-only feature, import the authentication helper and validate the role claim:

import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await auth();

  if (!session?.user) {
    return NextResponse.json({ error: "unauthenticated" }, { status: 401 });
  }

  // Admin-only operation
  if (session.user.role !== "ADMIN") {
    return NextResponse.json({ error: "forbidden" }, { status: 403 });
  }

  // …admin logic goes here…
  return NextResponse.json({ ok: true });
}

Authorizing Private Prompt Views

Use the centralized access helper to determine visibility without leaking the existence of private resources:

import { canViewPrompt } from "@/lib/prompt-access";
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function GET(req) {
  const { id } = req.params;
  const prompt = await db.prompt.findUnique({ where: { id } });
  const session = await auth();

  if (!canViewPrompt(prompt, session)) {
    // Hide existence – return 404
    return NextResponse.json({ error: "not_found" }, { status: 404 });
  }

  return NextResponse.json(prompt);
}

Summary

  • The RBAC system in prompts.chat relies on a Prisma UserRole enum with ADMIN and USER values, defaulting to USER for new accounts.
  • NextAuth.js propagates roles from the database into JWT tokens via src/lib/auth/index.ts, exposing session.user.role to all API handlers.
  • Admin-only endpoints under src/app/api/admin/* enforce hard stops with if (session.user.role !== "ADMIN") checks returning HTTP 403.
  • Content access is governed by canViewPrompt in src/lib/prompt-access.ts, which grants visibility to prompt owners or administrators while returning 404 for unauthorized private prompts.
  • Mutation rights in src/app/api/prompts/[id]/route.ts allow regular users to modify only their own data, while admins bypass ownership validation.

Frequently Asked Questions

How is the user role stored and accessed in prompts.chat?

The role is stored as a UserRole enum value in the PostgreSQL database via Prisma, then cached in the JWT token during authentication. The session.user.role property, populated in src/lib/auth/index.ts, makes the role available to all server-side API handlers without requiring repeated database lookups.

Can regular users access private prompts created by others?

No. The canViewPrompt helper in src/lib/prompt-access.ts explicitly denies access unless the requesting user is either the prompt owner (matching authorId) or has the ADMIN role. Unauthorized requests receive a 404 response to prevent information leakage about private content existence.

What prevents non-admin users from accessing user management APIs?

All administrative routes under src/app/api/admin/* implement an immediate authorization check: if (session.user.role !== "ADMIN") returns a 403 Forbidden response. This guard appears in files like src/app/api/admin/users/route.ts and protects all platform management operations.

How can I add a new admin-only feature to the codebase?

Create a new route under src/app/api/admin/ and implement the standard role guard pattern. Import auth from @/lib/auth, retrieve the session, and verify session.user.role === "ADMIN" before executing business logic. This ensures consistency with the existing user role and permissions system ADMIN vs USER in prompts.chat.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →