Access Control
Collection and field-level access control using JavaScript functions or Jexl expression strings.
Dyrected uses a rule-based access control system. Every collection and field defines its own rules as either a JavaScript function or a Jexl expression string — there is no global role registry or permission table.
How Access Works
Access functions are called on every request before any database operation. They receive the current user (from the JWT on the request) plus context, and return:
true— allow the operationfalse— deny (returns403 Forbidden)object— allow, but filter the results to only documents matching the object (used for row-level security)
type AccessFunction = (args: {
user: AuthenticatedUser | null
doc?: Record<string, any> // the existing document (on read/update/delete)
data?: Record<string, any> // the incoming data (on create/update)
req: HonoRequest
}) => boolean | object | Promise<boolean | object>Collection-Level Access
Define access rules on the access key of a CollectionConfig:
export const Posts = defineCollection({
slug: 'posts',
access: {
read: ({ user, doc }) => doc?.status === 'published' || !!user,
create: ({ user }) => ['editor', 'admin'].includes(user?.role),
update: ({ user, doc }) => user?.sub === doc?.authorId || user?.role === 'admin',
delete: ({ user }) => user?.role === 'admin',
},
fields: [...],
})| Operation | HTTP Method | Access Key |
|---|---|---|
| List & get single | GET | read |
| Create | POST | create |
| Update | PATCH | update |
| Delete | DELETE | delete |
Public Access
access: {
read: () => true, // anyone can read
create: () => true, // anyone can submit (e.g. a contact form)
update: () => false, // nobody can update
delete: () => false,
}Row-Level Filtering
When read returns an object, Dyrected treats it as a where clause and automatically filters the query:
access: {
// Users can only read their own orders
read: ({ user }) => {
if (!user) return false
if (user.role === 'admin') return true
return { customer: user.sub } // adds WHERE customer = user.sub
}
}The filter object uses the same shape as the where query parameter on the API.
Async Access
Access functions can be async — useful for lookups:
access: {
update: async ({ user, doc }) => {
const org = await db.findOne({ collection: 'orgs', id: doc.orgId })
return org?.members.includes(user?.sub)
}
}Field-Level Access
Fields can define their own access object. This is evaluated after collection-level access is granted.
{
name: 'internalNotes',
type: 'textarea',
access: {
read: ({ user }) => user?.role === 'admin',
update: ({ user }) => user?.role === 'admin',
}
}| Key | Effect on reads | Effect on writes |
|---|---|---|
read: () => false | Field is stripped from the response | — |
update: () => false | — | Field is silently ignored on write |
Field access functions receive the same { user, doc, data, req } args as collection access.
Admin UI behaviour:
- Fields where
readreturnsfalseare hidden from the edit form. - Fields where
updatereturnsfalseare rendered as read-only in the form.
Global-Level Access
Globals support read and update:
export const SiteSettings = defineGlobal({
slug: 'site-settings',
access: {
read: () => true,
update: ({ user }) => user?.role === 'admin',
},
fields: [...],
})Functions vs Jexl strings
Each access key accepts either a JavaScript function or a Jexl expression string. Both are evaluated server-side on every request.
access: {
// Function — full JS, async-capable, can return a where filter
update: ({ user, doc }) => user?.role === 'admin' || user?.sub === doc?.authorId,
// Jexl string — serialisable, storable in a database, works in Cloud schemas
delete: "user.role == 'admin'",
}| Function | Jexl string | |
|---|---|---|
| Async / DB lookups | Yes | No |
Row-level where filter | Yes (return an object) | No |
| Serialisable / Cloud-compatible | No | Yes |
| TypeScript autocomplete | Yes | No |
Use functions when you need async lookups or row-level filtering. Use Jexl strings when your access rules need to be stored or synced to Dyrected Cloud. Simple boolean checks work equally well as either.