Authentication
Built-in auth system with JWT sessions, RBAC, access control, and API key authentication.
Nextly ships with a complete authentication system built on Auth.js. It handles user sessions via JWT cookies, password hashing with bcrypt, and a full RBAC (Role-Based Access Control) layer with code-defined and database-managed permissions.
How Auth Works
Authentication flows through three layers:
- Auth.js integration -- credentials provider with JWT sessions stored in a
nextly_cms_sessioncookie - Session helpers --
getSession(),requireAuth(), role-checking utilities - RBAC middleware --
requirePermission()andrequireCollectionAccess()for fine-grained authorization
All admin routes are scoped to /admin/* with isolated cookie paths, so Nextly auth never interferes with your frontend application cookies.
Session Management
Sessions use the JWT strategy with a 30-day max age. The session token is stored in an httpOnly cookie named nextly_cms_session, scoped to the /admin path.
Getting the Current User
// app/api/my-route/route.ts
import { getSession } from "@nextlyhq/nextly/auth";
export async function GET(req: Request) {
const user = await getSession(req);
if (!user) {
return Response.json({ error: "Not authenticated" }, { status: 401 });
}
// user.id, user.email, user.name, user.roles
return Response.json({ userId: user.id, roles: user.roles });
}The SessionUser object contains:
| Field | Type | Description |
|---|---|---|
id | string | User's unique ID |
email | string | User's email address |
name | string? | Display name |
image | string? | Profile image URL |
roles | string[] | Array of role IDs from the JWT token |
Requiring Authentication
import { requireAuth } from "@nextlyhq/nextly/auth";
export async function GET(req: Request) {
// Throws "Authentication required" if no session
const user = await requireAuth(req);
return Response.json({ email: user.email });
}Roles
Roles are managed through the admin UI (Settings > Roles) or programmatically via the RoleService. Every role has a name, slug, level, and an optional description.
The super-admin role is a built-in system role that automatically bypasses all permission checks. It is created on first startup via ensureSuperAdminRole().
Checking Roles in Code
import { getSession, hasRole, hasAnyRole, hasAllRoles } from "@nextlyhq/nextly/auth";
export async function GET(req: Request) {
const user = await getSession(req);
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
// Single role check
if (hasRole(user, "admin")) {
// user has the "admin" role
}
// Any of these roles (OR logic)
if (hasAnyRole(user, ["admin", "editor"])) {
// user has at least one of these roles
}
// All of these roles (AND logic)
if (hasAllRoles(user, ["editor", "reviewer"])) {
// user has both roles
}
}Requiring a Specific Role
import { requireRole, requireAnyRole } from "@nextlyhq/nextly/auth";
export async function DELETE(req: Request) {
// Throws if user does not have "admin" role
const user = await requireRole(req, "admin");
// Or require any of several roles
const editor = await requireAnyRole(req, ["admin", "editor"]);
}Permissions
Permissions are defined as action + resource pairs (e.g., create + posts, read + users). They are assigned to roles, and roles are assigned to users.
The four standard actions are: create, read, update, delete.
Permissions are auto-seeded when you create collections or singles, and managed through the admin UI or PermissionService.
Permission-Based Authorization
The requirePermission() middleware checks the full RBAC chain:
- Session or API key authentication
- Super-admin bypass
- Code-defined access functions (from
defineCollection) - Database role/permission check
import { requirePermission, isErrorResponse, createJsonErrorResponse } from "@nextlyhq/nextly/auth";
export async function POST(req: Request) {
const result = await requirePermission(req, "create", "posts");
if (isErrorResponse(result)) {
return createJsonErrorResponse(result);
}
// result is AuthContext: { userId, permissions, roles, authMethod }
const { userId } = result;
// ... create the post
}Code-Defined Access Control
You can define access rules directly on collections and singles using defineCollection({ access }). These take precedence over database permissions (but super-admin always bypasses everything).
// src/collections/posts.ts
import { defineCollection } from "@nextlyhq/nextly/config";
export default defineCollection({
slug: "posts",
access: {
// Boolean: allow any authenticated user to read
read: true,
// Function: only admins and editors can create
create: ({ roles }) => roles.includes("admin") || roles.includes("editor"),
// Function: only admins and editors can update
update: ({ roles }) => roles.includes("admin") || roles.includes("editor"),
// Function: only admins can delete
delete: ({ roles }) => roles.includes("admin"),
},
fields: [
// ...
],
});The access function receives an AccessControlContext:
interface AccessControlContext {
user: { id: string; email?: string } | null;
roles: string[]; // role slugs (resolved from DB, includes inherited roles)
permissions: string[]; // effective permission slugs
operation: "create" | "read" | "update" | "delete";
collection: string; // collection or single slug
}For singles, only read and update are supported:
import { defineSingle } from "@nextlyhq/nextly/config";
export default defineSingle({
slug: "site-settings",
access: {
read: true,
update: ({ roles }) => roles.includes("admin"),
},
fields: [
// ...
],
});API Key Authentication
Nextly supports API key authentication via the Authorization: Bearer sk_live_... header. API keys are created and managed through the admin UI (Settings > API Keys).
Token Types
| Type | Description |
|---|---|
read-only | Only read-* permission slugs |
full-access | All permissions of the key creator's roles |
role-based | Permissions of a specific role |
How It Works
When a request includes a Bearer token, the auth middleware:
- Hashes the key with SHA-256 and looks it up in the database
- Checks if the key is expired or revoked
- Enforces per-key rate limiting (default: 1,000 requests/hour)
- Resolves permissions based on the token type
- Returns an
AuthContextwithauthMethod: "api-key"
// Client-side usage
const response = await fetch("https://example.com/api/nextly/collections/posts/entries", {
headers: {
Authorization: "Bearer sk_live_abc123...",
},
});Configuring API Key Rate Limits
// nextly.config.ts
export default defineConfig({
apiKeys: {
rateLimit: {
requestsPerHour: 500, // default: 1000
windowMs: 3_600_000, // default: 1 hour
},
},
});Password Management
Passwords are hashed using bcrypt with 12 salt rounds. The AuthService provides methods for the complete password lifecycle:
registerUser()-- creates a new user with validated passwordverifyCredentials()-- validates email/password for loginchangePassword()-- updates password after verifying current onegeneratePasswordResetToken()-- creates a SHA-256 hashed token (24-hour expiry)resetPasswordWithToken()-- consumes the token and updates the passwordgenerateEmailVerificationToken()-- creates an email verification tokenverifyEmail()-- marks email as verified
Token security: raw tokens are returned to the user once, but only SHA-256 hashes are stored in the database.
Environment Variables
| Variable | Required | Description |
|---|---|---|
AUTH_SECRET | Yes | JWT signing secret (min 32 characters) |
NEXTAUTH_URL | Yes | Your app's public URL |
AUTH_TRUST_HOST | No | Trust host header (default: true) |
Generate a secure AUTH_SECRET:
openssl rand -base64 32Next Steps
- Email Configuration -- set up password reset and verification emails
- Collections -- configure access control on collections
- Configuration Reference -- full
defineConfig()options - REST API -- REST API endpoints for auth flows