Overview
How Dyrected decides who can read and write your data — the rules you attach to collections, globals, and fields.
Access control is how you decide who can do what with your data. In Dyrected you write that as small rules attached directly to a collection, a global, or a field. There is no separate permissions table and no global role registry to keep in sync — the rule lives next to the thing it protects, and Dyrected runs it on the server for every request.
This page is the model: what a rule is, the shapes it can take, when it runs, and what it can see. The pages that follow show where to put rules for collections, globals, and fields. If you are deploying to Dyrected Cloud, also read Dyrected Cloud access control for the Cloud-specific policy limits and built-in policy names.
In self-hosted Dyrected, access rules run inside your application backend. Use Jexl strings when they are enough, and use function rules or function policies when the decision needs your server runtime, database boundary, or application-user model.
The mental model
An access rule answers one question: should this request be allowed? Dyrected evaluates the rule on the server, before it touches the database, and reads the result in three ways:
- A truthy result allows the operation.
- A
falseresult denies it — Dyrected responds with403 Forbidden. - An object result allows the operation but scopes it to matching documents — a row-level filter, covered in Collections.
A denied request always comes back in the same shape, so your frontend can detect it reliably:
{ "error": true, "message": "Access denied: read on posts" }Because the check runs before the database call, a denied read never loads the document and a denied write never persists anything.
The rule shapes
Every access rule can be written in one of four ways. They all produce the same allow / deny / filter result, so you can mix them freely across a project — and even across operations on the same collection.
access: {
read: true, // 1. boolean
create: "'admin' in user.roles", // 2. Jexl string
update: ({ user, doc }) => // 3. function
user?.roles?.includes('admin') || user?.sub === doc?.authorId,
delete: { policy: 'isAdmin' }, // 4. named policy
}Pick the shape by what the rule needs to do and where it needs to run:
| Shape | Reach for it when | Runs on |
|---|---|---|
| Boolean | The answer never changes — always open or always closed. | — |
| Jexl string | A serializable check like a role test or field comparison. Safe to sync to Dyrected Cloud. | Server |
| Function | You need full server logic — async lookups, complex conditions, computed filters. | Self-hosted server only |
| Named policy | You want a reusable rule you can name once and reference everywhere. Serializable policies work with Cloud; function policies stay self-hosted. | Server |
Booleans and Jexl strings cover most day-to-day rules. Reach for functions when a self-hosted app needs real logic, and named policies when you catch yourself repeating the same rule.
Jexl strings
A Jexl string is a small expression that Dyrected evaluates on the server. It reads like JavaScript comparison syntax, but it is a restricted, sandboxed language: it cannot call functions, import anything, or reach outside the values you hand it — which is exactly what makes it safe to store and sync to Cloud.
Your rule has the access context in scope (user, req, doc, data, id), plus these building blocks:
| You want to | Write | Example |
|---|---|---|
| Compare values | == != > >= < <= | doc.status == 'published' |
| Combine conditions | && || ! | user != null && !doc.locked |
| Check membership | in | 'admin' in user.roles |
| Read a property | . or ['key'] | user.roles, doc['status'] |
| Pick between values | cond ? a : b | user ? true : false |
Two things to remember: use == and != (not ===), and Dyrected registers no custom Jexl functions or transforms, so stick to the operators above.
A rule that evaluates to a boolean allows or denies:
access: {
read: "user != null", // any signed-in user
update: "'admin' in user.roles || 'editor' in user.roles",
}A rule that evaluates to an object becomes a row-level where filter — "{ owner: { equals: user.sub } }" limits the operation to the caller's own documents (covered in Collections). A ternary lets one rule return either shape, so it can hand signed-in users everything and scope everyone else:
access: {
// signed-in users see everything; anonymous readers only see published docs
read: "user != null ? true : { status: { equals: 'published' } }",
}For the complete expression syntax, see the Jexl reference.
Functions
A function gives you the full server runtime, including async work. It receives the same context as every other rule and can return a boolean or a filter object:
access: {
update: async ({ user, doc }) => {
if (!user) return false
return user.roles?.includes('admin') || doc?.authorId === user.sub
},
}Functions run only in your self-hosted server — they are not synced to Dyrected Cloud. When you run sync-schema, the CLI strips function rules from the payload and warns you with their exact paths. For any rule that needs to reach Cloud, use a Jexl string or a named policy instead.
Dyrected also validates declarative access expressions early. If a Jexl rule or string-based policy uses unsupported context or invalid syntax, Dyrected points to the exact config path so you can fix it before sync or runtime.
Named policies
When the same rule shows up on several collections, define it once under accessPolicies and reference it by name.
In self-hosted Dyrected, that policy lives on your server config. String and boolean policies are portable, while function policies can use your application server.
export default defineConfig({
accessPolicies: {
isAdmin: "'admin' in user.roles",
},
collections: [
defineCollection({
slug: 'posts',
access: {
create: { policy: 'isAdmin' },
delete: { policy: 'isAdmin' },
},
fields: [{ name: 'title', label: 'Title', type: 'text' }],
}),
],
globals: [],
})A policy can be a function or a Jexl string — accessPolicies: { isAdmin: "'admin' in user.roles" }. String policies are inlined when the schema is sent to the admin panel, so a field or collection rule that references one stays reactive in the edit form, exactly like an inline Jexl rule. Function policies run on the server only and are not Cloud-portable.
You can pass params for a parameterized policy — { policy: 'hasRole', params: { role: 'editor' } } — and read them inside the resolver as params. If a rule references a policy that is not registered, Dyrected fails closed and denies the request.
What a policy receives
A function-based policy resolver receives the same access context as a normal access function, plus params from the policy reference.
| Value | What it is |
|---|---|
user | The authenticated caller, or undefined for an anonymous request |
req | The request context |
doc | The existing document, when Dyrected is checking one |
data | The incoming payload, on create and update |
params | Extra values passed by { policy: 'name', params: { ... } } |
That means a parameterized policy can stay reusable while still reacting to the current request:
export default defineConfig({
accessPolicies: {
hasRole: ({ user, params }) => {
const role = typeof params?.role === 'string' ? params.role : null
return role ? user?.roles?.includes(role) ?? false : false
},
},
collections: [
defineCollection({
slug: 'posts',
access: {
update: { policy: 'hasRole', params: { role: 'editor' } },
},
fields: [{ name: 'title', label: 'Title', type: 'text' }],
}),
],
globals: [],
})If the policy value is a Jexl string or a boolean, there is no function parameter list, but the Jexl expression still reads the normal access context such as user, doc, and data.
The same rule, three ways
These rules are equivalent — each one says "a user can only touch their own documents." The result is identical; the shapes differ in where they can run and how reusable they are.
Serializable and Cloud-safe — a good default.
access: {
read: "{ owner: { equals: user.sub } }",
}Defined once, referenced everywhere. A string policy is inlined for the admin panel and can sync to Cloud; a function policy stays on the server.
// defineConfig({ accessPolicies: { ownDocs: "{ owner: { equals: user.sub } }" } })
access: {
read: { policy: 'ownDocs' },
}Full server logic, including async work and lookups.
access: {
read: ({ user }) => ({ owner: { equals: user?.sub } }),
}Function rules run only in a self-hosted app, where your own server executes your config. They are never sent to Dyrected Cloud — sync-schema strips them and warns you. For a Cloud-hosted backend, use a Jexl string or a string/boolean named policy instead.
What a rule can see
Every rule is evaluated with this context in scope:
| Variable | What it is | Available on |
|---|---|---|
user | The authenticated user, or undefined if the request is anonymous | Always |
req | The request context (headers, query parameters) | Always |
doc | The existing document | Reads and writes of a specific document |
data | The incoming payload | Create and update |
id | The target document's ID | Operations on a specific document |
The user object is the caller's own record, loaded fresh from the database on each request, so it includes whatever fields your auth collection defines. A few properties are always present:
| Property | Type | Meaning |
|---|---|---|
user.sub / user.id | string | The user's document ID |
user.email | string | The user's email address |
user.collection | string | The auth collection the user signed in against |
user.roles | string[] | Role strings, when your auth collection has a roles field |
The scaffolded auth collection ships with a roles field offering admin, editor, and viewer, which is why 'admin' in user.roles is the idiom you will see most often. For the full picture of what lands on user, see Token data.
Guarding against anonymous requests is a matter of checking whether user exists:
access: {
read: "user != null", // any signed-in user
}Where rules live
Access rules attach at three levels, each with its own operations. All three are enforced on the server, on every request:
| Level | Operations | What it protects |
|---|---|---|
| Collections | read, create, update, delete, readAudit | Whole documents in a collection, plus the audit log |
| Globals | read, update | A single global document |
| Fields | read, create, update | Individual fields — stripped from responses or dropped on writes |
Named policies do not attach to content on their own. You define them once under accessPolicies, then reference them from collection, global, or field rules.
Field rules also drive the admin panel, hiding or locking fields in the edit form, but the enforcement is real: a field a reader cannot see is removed from the API response, and a field a writer cannot set is dropped before the document is saved.
Default access
If you do not set a rule for an operation, that operation is open. A collection with no access block can be read, created, updated, and deleted by anyone who can reach the API.
Access is open by default, not locked down. Set explicit rules on anything that holds data you would not publish publicly, and treat a missing rule as "public" while you review your config. To turn an operation off entirely, set it to false rather than omitting it.
A good habit is to start each collection closed and open up only what an operation needs:
access: {
read: "user != null",
create: "'admin' in user.roles",
update: "'admin' in user.roles",
delete: "'admin' in user.roles",
}Where to go next
- Collection access control — the four operations, row-level filtering, and common recipes.
- Global access control — read and update rules for single-document config.
- Field access control — hiding and locking individual fields.
- Hooks — for logic that needs to transform data or raise custom errors, not just allow or deny.