You're reading docs for Nextly Alpha. APIs may change between releases.

Guides

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:

  1. Auth.js integration -- credentials provider with JWT sessions stored in a nextly_cms_session cookie
  2. Session helpers -- getSession(), requireAuth(), role-checking utilities
  3. RBAC middleware -- requirePermission() and requireCollectionAccess() 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:

FieldTypeDescription
idstringUser's unique ID
emailstringUser's email address
namestring?Display name
imagestring?Profile image URL
rolesstring[]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:

  1. Session or API key authentication
  2. Super-admin bypass
  3. Code-defined access functions (from defineCollection)
  4. 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

TypeDescription
read-onlyOnly read-* permission slugs
full-accessAll permissions of the key creator's roles
role-basedPermissions of a specific role

How It Works

When a request includes a Bearer token, the auth middleware:

  1. Hashes the key with SHA-256 and looks it up in the database
  2. Checks if the key is expired or revoked
  3. Enforces per-key rate limiting (default: 1,000 requests/hour)
  4. Resolves permissions based on the token type
  5. Returns an AuthContext with authMethod: "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 password
  • verifyCredentials() -- validates email/password for login
  • changePassword() -- updates password after verifying current one
  • generatePasswordResetToken() -- creates a SHA-256 hashed token (24-hour expiry)
  • resetPasswordWithToken() -- consumes the token and updates the password
  • generateEmailVerificationToken() -- creates an email verification token
  • verifyEmail() -- 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

VariableRequiredDescription
AUTH_SECRETYesJWT signing secret (min 32 characters)
NEXTAUTH_URLYesYour app's public URL
AUTH_TRUST_HOSTNoTrust host header (default: true)

Generate a secure AUTH_SECRET:

openssl rand -base64 32

Next Steps