Dyrected
Core Concepts

Access Control

Function-based access control at the collection level and field level.

Dyrected uses a function-based access control system. Every collection and field defines its own rules as plain JavaScript functions — 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?.id === 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.id }   // adds WHERE customer = user.id
  }
}

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?.id)
  }
}

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: [...],
})

Common Patterns

Only authenticated users can write

access: {
  read:   () => true,
  create: ({ user }) => !!user,
  update: ({ user }) => !!user,
  delete: ({ user }) => !!user,
}

Admins can do everything; editors can read and update; viewers read-only

access: {
  read:   ({ user }) => !!user,
  create: ({ user }) => ['admin', 'editor'].includes(user?.role),
  update: ({ user }) => ['admin', 'editor'].includes(user?.role),
  delete: ({ user }) => user?.role === 'admin',
}

Authors own their content

access: {
  read:   ({ user, doc }) => doc?.status === 'published' || !!user,
  create: ({ user }) => !!user,
  update: ({ user, doc }) => user?.role === 'admin' || user?.id === doc?.authorId,
  delete: ({ user }) => user?.role === 'admin',
}

Public contact form

access: {
  create: () => true,       // anyone can submit
  read:   ({ user }) => !!user,  // only logged-in users can read submissions
  update: () => false,
  delete: ({ user }) => user?.role === 'admin',
}

On this page