Dyrected
Model ContentContent RulesAccess Control

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 false result denies it — Dyrected responds with 403 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:

ShapeReach for it whenRuns on
BooleanThe answer never changes — always open or always closed.
Jexl stringA serializable check like a role test or field comparison. Safe to sync to Dyrected Cloud.Server
FunctionYou need full server logic — async lookups, complex conditions, computed filters.Self-hosted server only
Named policyYou 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 toWriteExample
Compare values== != > >= < <=doc.status == 'published'
Combine conditions&& || !user != null && !doc.locked
Check membershipin'admin' in user.roles
Read a property. or ['key']user.roles, doc['status']
Pick between valuescond ? a : buser ? 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 stringaccessPolicies: { 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.

ValueWhat it is
userThe authenticated caller, or undefined for an anonymous request
reqThe request context
docThe existing document, when Dyrected is checking one
dataThe incoming payload, on create and update
paramsExtra 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:

VariableWhat it isAvailable on
userThe authenticated user, or undefined if the request is anonymousAlways
reqThe request context (headers, query parameters)Always
docThe existing documentReads and writes of a specific document
dataThe incoming payloadCreate and update
idThe target document's IDOperations 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:

PropertyTypeMeaning
user.sub / user.idstringThe user's document ID
user.emailstringThe user's email address
user.collectionstringThe auth collection the user signed in against
user.rolesstring[]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:

LevelOperationsWhat it protects
Collectionsread, create, update, delete, readAuditWhole documents in a collection, plus the audit log
Globalsread, updateA single global document
Fieldsread, create, updateIndividual 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

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