Dyrected
Model ContentContent RulesAccess Control

Collections

Read, create, update, and delete rules that decide who can touch a collection's documents.

Collection access control is where you lock down a collection's data. You attach up to four rules — one per operation — to the access key of a collection, and Dyrected runs the matching rule on the server before every request.

Read the Access Control overview first for the shared model — the four rule shapes, the context a rule receives, and the allow / deny / filter result. This page focuses on the four collection operations and how to use them in real projects.

For self-hosted collections, function rules are available because Dyrected runs inside your application backend. Use them when access depends on local services, database-specific checks, or application-user behavior that cannot be represented as a Jexl expression.

The shape

All four rules live under access. Here is a collection that lets any signed-in user read, restricts writing to editors, and reserves deletion for admins:

import { defineCollection } from '@dyrected/core'

export const Posts = defineCollection({
  slug: 'posts',
  access: {
    read: 'user != null',
    create: "'admin' in user.roles || 'editor' in user.roles",
    update: "'admin' in user.roles || 'editor' in user.roles",
    delete: "'admin' in user.roles",
  },
  fields: [
    // ...
  ],
})

Each key is optional. Any operation you leave out is open — see Default access below.

How operations map to requests

Each rule guards one operation, which maps to an HTTP method on the collection's API:

OperationHTTP methodAccess key
List documents and fetch one by IDGETread
Create a documentPOSTcreate
Update a documentPATCHupdate
Delete a documentDELETEdelete
Read the audit log (audit-enabled collections)GET /__auditreadAudit

Listing and fetching a single document share the same read rule, so a reader either can or cannot see the collection's documents through the API. For the full set of endpoints and their query parameters, see the REST API overview.

The four operations

Read

read decides who can list a collection and fetch its documents. Use it to draw the line between public content and content that requires a login.

access: {
  read: true, // published content anyone can fetch
}

To require authentication instead, check for a user:

access: {
  read: 'user != null',
}

Create

create decides who can add new documents. A common pattern is to open creation to the public for something like a contact form while keeping everything else staff-only:

access: {
  create: true, // e.g. a public submission form
}

Or restrict it to a role:

access: {
  create: "'admin' in user.roles || 'editor' in user.roles",
}

Update

update decides who can modify existing documents:

access: {
  update: "'admin' in user.roles || 'editor' in user.roles",
}

Delete

delete decides who can remove documents. Deletion is usually the most restricted operation, so reserving it for admins is a sensible default:

access: {
  delete: "'admin' in user.roles",
}

What a collection rule receives

Collection rules run on the server with the standard access context:

  • user — the authenticated caller
  • req — request metadata
  • doc — the existing document, when Dyrected is checking a specific one (fetch, update, delete)
  • data — the incoming payload, on create and update
  • id — the target document's ID, on operations against a specific document

That is enough for both broad role checks and document-aware rules.

Document-aware rules

For ownership or tenant scoping — "users only see their own orders," "editors only touch their team's posts" — return a where-style object instead of a boolean.

Any of the rule shapes can do this: a function, a named policy, or a Jexl string that evaluates to an object.

access: {
  // A user can only read and modify documents they own
  read: "{ author: { equals: user.sub } }",
  update: "{ author: { equals: user.sub } }",
  delete: "{ author: { equals: user.sub } }",
}
access: {
  // A user can only read and modify documents they own
  read: ({ user }) => ({ author: { equals: user?.sub } }),
  update: ({ user }) => ({ author: { equals: user?.sub } }),
  delete: ({ user }) => ({ author: { equals: user?.sub } }),
}

Dyrected applies the returned filter differently per operation:

  • List — merges the filter into the query, so the response only contains matching documents.
  • Fetch one, update, delete — checks whether the target document matches the filter; if it does not, the request is denied with 403.

Keep create rules boolean — there is no existing document to match a filter against, so a create rule that returns an object is treated as a denial. To gate creation on the incoming payload, read data and return a boolean.

access: {
  create: "'editor' in user.roles && data.status != 'published'",
}
access: {
  create: ({ user, data }) =>
    (user?.roles?.includes('editor') ?? false) && data?.status !== 'published',
}

Audit log access

When a collection has audit enabled, Dyrected records every change and exposes it at GET /:slug/__audit. By default that log follows the collection's read rule — whoever can read the documents can read their history. To gate the audit trail separately, add a readAudit rule:

export const Posts = defineCollection({
  slug: 'posts',
  audit: true,
  access: {
    read: true,                          // anyone can read posts
    readAudit: "'admin' in user.roles",  // but only admins can read the audit log
  },
  fields: [
    // ...
  ],
})

readAudit takes the same shapes as any other rule and, like read, honors a returned filter object — the audit log is scoped to the documents the caller is allowed to see. Leave it out and the audit log simply inherits read.

Common setups

Anyone can read. Editors and admins can create and update. Only admins can delete.

export const Posts = defineCollection({
  slug: 'posts',
  access: {
    read: true,
    create: "'admin' in user.roles || 'editor' in user.roles",
    update: "'admin' in user.roles || 'editor' in user.roles",
    delete: "'admin' in user.roles",
  },
  fields: [
    // ...
  ],
})

Anyone can submit. Only signed-in staff can read submissions. Nobody edits or deletes through the API.

export const Inquiries = defineCollection({
  slug: 'inquiries',
  access: {
    create: true,
    read: 'user != null',
    update: false,
    delete: "'admin' in user.roles",
  },
  fields: [
    { name: 'name', label: 'Name', type: 'text', required: true },
    { name: 'email', label: 'Email', type: 'email', required: true },
    { name: 'message', label: 'Message', type: 'textarea' },
  ],
})

A three-tier model: viewers read, editors write, admins delete. The scaffolded auth collection already offers these three roles.

export const Articles = defineCollection({
  slug: 'articles',
  access: {
    read: 'user != null',
    create: "'admin' in user.roles || 'editor' in user.roles",
    update: "'admin' in user.roles || 'editor' in user.roles",
    delete: "'admin' in user.roles",
  },
  fields: [
    // ...
  ],
})

Each user only sees and edits their own documents; admins are not special-cased here, so add a role check if they need to see everything.

export const Orders = defineCollection({
  slug: 'orders',
  access: {
    read: ({ user }) => ({ customer: { equals: user?.sub } }),
    create: 'user != null',
    update: ({ user }) => ({ customer: { equals: user?.sub } }),
    delete: ({ user }) => ({ customer: { equals: user?.sub } }),
  },
  fields: [
    { name: 'customer', label: 'Customer', type: 'text' },
    { name: 'total', label: 'Total', type: 'number' },
  ],
})

The public sees only published documents; signed-in staff see every status. A Jexl string can return a boolean or a filter object, so a ternary handles both cases without a function:

export const Guides = defineCollection({
  slug: 'guides',
  access: {
    // signed-in → everything; anonymous → only published documents
    read: "user != null ? true : { status: { equals: 'published' } }",
    create: "'editor' in user.roles || 'admin' in user.roles",
    update: "'editor' in user.roles || 'admin' in user.roles",
    delete: "'admin' in user.roles",
  },
  fields: [
    { name: 'title', label: 'Title', type: 'text' },
    { name: 'status', label: 'Status', type: 'select', options: ['draft', 'published'] },
  ],
})

Every operation is scoped to the caller's tenant through a reusable named policy, registered once and referenced by name. Deletion is reserved for admins with a second policy:

// Registered once in defineConfig:
// accessPolicies: {
//   sameTenant: "{ tenant: { equals: user.tenant } }",
//   isAdmin: "'admin' in user.roles",
// }

export const Invoices = defineCollection({
  slug: 'invoices',
  access: {
    read: { policy: 'sameTenant' },
    create: 'user != null',
    update: { policy: 'sameTenant' },
    delete: { policy: 'isAdmin' },
  },
  fields: [
    { name: 'tenant', label: 'Tenant', type: 'text' },
    { name: 'amount', label: 'Amount', type: 'number' },
  ],
})

Default access

Any operation without a rule is open. A collection with no access block at all can be read, created, updated, and deleted by anyone who can reach the API.

Access is open by default. Set explicit rules on every collection that holds data you would not publish publicly. To turn an operation off entirely, set it to false rather than omitting it.

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend