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 Cloud-backed collections, keep rules portable: booleans, Jexl strings, or named policies backed by serializable policy values. If an example uses a function rule, convert it before relying on it in Cloud.
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:
| Operation | HTTP method | Access key |
|---|---|---|
| List documents and fetch one by ID | GET | read |
| Create a document | POST | create |
| Update a document | PATCH | update |
| Delete a document | DELETE | delete |
| Read the audit log (audit-enabled collections) | GET /__audit | readAudit |
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 callerreq— request metadatadoc— the existing document, when Dyrected is checking a specific one (fetch, update, delete)data— the incoming payload, on create and updateid— 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.
In Cloud, use a Jexl string or a Cloud-backed named policy for document-aware rules.
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 } }",
}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'",
}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: "{ customer: { equals: user.sub } }",
create: 'user != null',
update: "{ customer: { equals: user.sub } }",
delete: "{ 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.
Related pages
- Access control overview — the model behind these rules.
- Field access control — control field-level reads and writes.
- Collection configuration — everything else you can set on a collection.
- Collection hooks — for logic that transforms data or raises custom errors, beyond allow/deny.