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

> Learn how prompts.chat implements its user role and permissions system using RBAC with ADMIN and USER roles. Secure your access with our detailed guide.

- Repository: [Fatih Kadir Akın/prompts.chat](https://github.com/f/prompts.chat)
- Tags: deep-dive
- Published: 2026-04-02

---

**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:

```prisma
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`](https://github.com/f/prompts.chat/blob/main/src/lib/auth/index.ts), the `jwt` callback extracts the role from the user record and attaches it to the token:

```typescript
// 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:

```typescript
// 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`](https://github.com/f/prompts.chat/blob/main/src/app/api/admin/users/route.ts) short-circuits non-admin requests immediately:

```typescript
// 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`](https://github.com/f/prompts.chat/blob/main/src/lib/prompt-access.ts) provides the `canViewPrompt` helper that implements owner-or-admin logic:

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```typescript
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`](https://github.com/f/prompts.chat/blob/main/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`](https://github.com/f/prompts.chat/blob/main/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`](https://github.com/f/prompts.chat/blob/main/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`](https://github.com/f/prompts.chat/blob/main/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`](https://github.com/f/prompts.chat/blob/main/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.