Skip to content

ACL

ACL is opt-in per schema. Without a single acl declaration, every schema is owner-only — userId from the JWT scopes every read and write. Once you opt in, two scopes apply:

ScopeWhere declaredWhat it does
Document-levelschema.acl.{ list, delete }Listed roles bypass the userId filter for the named operation.
Field-levelfield.acl.{ read, create, update }Listed roles see / write the field; everyone else sees responses with the field stripped, and writes that supply the field server-side rejected.

Roles travel in the JWT’s roles claim. The default User model issues ['user'] on registration; admin / staff / hr roles are your responsibility to assign (typically a custom route or a script operated by a trusted admin).

module.exports = {
path: 'order',
fields: [/* ... */],
acl: {
list: ['admin', 'support'], // see across tenants on list / find / search
delete: ['admin'], // delete records they don't own
},
};
SlotBypass on
listList endpoints (GET /api/v1/<path>), findMany / findOne / count resolvers, list_<path> MCP tool, full-text search, audit-log history, aggregations.
deleteDELETE by id, <path>RemoveById GraphQL, delete_<path> MCP. Restore inherits from delete.

Without a slot, that operation stays owner-only. Only callers whose JWT carries one of the listed roles bypass the userId filter; any other role is treated as owner-only.

There’s deliberately no create or update slot at the document level — write operations always stamp userId/accountId from the caller’s JWT, so cross-tenant writes are structurally impossible. Field-level ACL is the right tool when only some roles can set certain fields.

{ name: 'salary', type: Number, acl: { read: ['admin', 'hr'] } }
{ name: 'tags', type: [String], acl: { update: ['admin'] } }
{ name: 'notes', type: String, acl: { create: ['admin'], update: ['admin'] } }
SlotEffect
readThe field is stripped from REST responses, GraphQL output, MCP tool results, search snippets, history rows, and webhook payloads for callers without an overlapping role.
createIf a caller without an overlapping role supplies the field on POST, the framework strips it server-side.
updateSame, but on PUT / GraphQL <path>UpdateById / MCP update_<path>.

create and update strip rather than reject so an agent isn’t trapped by a payload it didn’t know was sensitive — same posture as tenant-stamped fields. If you need hard rejection, write a custom route.

The same projectByAcl helper runs everywhere:

SurfaceWhere ACL projection runs
REST list / get / by-idAfter the find.
REST POST / PUTBefore the write (input filter).
GraphQL output<path>Type.applyAclProjection resolver wrapper.
GraphQL inputfilterWritable strips non-writable fields.
MCP list_* / get_*Same projector as REST.
MCP create_* / update_*Same input filter.
Search resultsBefore the response.
Relations (__include)Each populated record runs through the target schema’s projector.
Audit log historyBefore / after / diff projections all ACL-filtered.
Outbound webhook payloadsSame projector as audit.

There’s no side channel that bypasses ACL.

{
"user_id": "65a0...",
"email": "admin@example.com",
"roles": ["admin"]
}

The framework reads req.user.roles (REST) and ctx.user.roles (GraphQL / MCP). Missing or empty roles defaults to ['user']. Issuing roles is up to your application — routes/auth/ is hand-written, so attach roles in your custom registration / admin-promotion logic.

A common pattern: admin operators see all rows, but salary stays visible only to admin and HR.

acl: { list: ['admin', 'support'] },
fields: [
/* ... */
{ name: 'salary', type: Number, acl: { read: ['admin', 'hr'] } },
],

A support user can list rows from any tenant, but salary is stripped. An admin user sees every row and every field. An hr user is owner-only on the document level but sees salary on rows they own.

  • Not row-level access control beyond owner / role. dAvePi has owner-only or role-bypass; there’s no “user A can read this specific document of user B” granularity. If you need that, layer a sharing collection on top.
  • Not OAuth scopes. Roles are coarse — admin / hr / support / user. For fine-grained API scopes, generate scoped tokens at the auth layer and check them in custom routes.
  • Not policy-as-code. No CEL / Rego / OPA. The expressiveness is what fits in the schema vocabulary.