Dyrecteddyrected
Core Concepts

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 operation
  • false — deny (returns 403 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: [...],
})
OperationHTTP MethodAccess Key
List & get singleGETread
CreatePOSTcreate
UpdatePATCHupdate
DeleteDELETEdelete

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',
  }
}
KeyEffect on readsEffect on writes
read: () => falseField is stripped from the response
update: () => falseField is silently ignored on write

Field access functions receive the same { user, doc, data, req } args as collection access.

Admin UI behaviour:

  • Fields where read returns false are hidden from the edit form.
  • Fields where update returns false are 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'",
}
FunctionJexl string
Async / DB lookupsYesNo
Row-level where filterYes (return an object)No
Serialisable / Cloud-compatibleNoYes
TypeScript autocompleteYesNo

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.


Common Patterns

On this page