Permissions

Role-based access control defined in code.

Roles and permissions are defined in your cms.config.ts — not stored in the database. This keeps them version-controlled, reviewable, and deployable.

Defining roles


          import { role } from "@cms/config";

// Full admin — bypasses all permission checks
const admin = role("admin", {
  admin: true,
});

// Authenticated role with granular permissions
const editor = role("editor", {
  permissions: {
    posts: {
      create: true,
      read: true,
      update: {
        filter: { author: "$CURRENT_USER" },
        fields: { exclude: ["status"] },
      },
      delete: false,
    },
    _media: {
      create: true,
      read: true,
      update: true,
      delete: false,
    },
  },
});

// Public role — unauthenticated API access
const publicRole = role("public", {
  public: true,
  permissions: {
    posts: {
      read: {
        filter: { status: "published" },
        fields: { exclude: ["internalNotes"] },
      },
    },
    pages: {
      read: {
        filter: { status: "published" },
      },
    },
  },
});
        

Permission rules

Each CRUD operation can be:

  • true — allowed with no restrictions
  • false — denied
  • An object with filter and/or fields — allowed with constraints

          {
  create?: boolean | PermissionRule;
  read?: boolean | PermissionRule;
  update?: boolean | PermissionRule;
  delete?: boolean | PermissionRule;
}
        

Item-level filters

Filters restrict which records the role can access. They’re appended as WHERE clauses automatically:


          update: {
  filter: { author: "$CURRENT_USER" },
}
        

A public request to GET /api/posts with filter: { status: "published" } automatically adds WHERE status = 'published'.

Field-level restrictions

Control which fields are readable or writable:


          read: {
  fields: { exclude: ["internalNotes", "draftContent"] },
}
        

Excluded fields are stripped from API responses and rejected in create/update requests.

Variables

Use variables in permission filters for dynamic values:

VariableResolves to
$CURRENT_USERThe authenticated user’s ID
$CURRENT_ROLEThe authenticated user’s role name
$NOWCurrent ISO 8601 timestamp

          // Only allow editing posts you authored
update: {
  filter: { author: "$CURRENT_USER" },
}

// Only show published items or items where the user is the author
read: {
  filter: {
    $or: [
      { status: "published" },
      { author: "$CURRENT_USER" },
    ],
  },
}
        

How permissions are enforced

Permissions are applied at the query layer — every API request is scoped by the user’s role:

  1. CRUD booleans gate the entire operation. If delete: false, the DELETE endpoint returns 403.
  2. Item-level filters are merged into the query’s WHERE clause. The user never sees records outside their filter scope.
  3. Field-level restrictions control which columns appear in SELECT and which are accepted in INSERT/UPDATE.

Admin roles (admin: true) bypass all checks.

Unauthenticated requests use the role marked public: true. If no public role exists, unauthenticated requests get 401.

Collections without permissions

If a collection is not mentioned in a role’s permissions, that role has no access to it. Permissions are deny-by-default.

Previous

Querying

Next

Hooks