Skip to content

Restrict a field to specific roles

A user’s phoneNumber should be visible to admins only, but writable by every user (for self-service settings). A handler-level role check is too coarse — the check needs to happen at the field, not the call.

  • You’ve read Auth and permissions so the three-layer model is clear.
  • Your feature already declares roles.

Field-level access lives on the field definition itself:

src/features/users/schema/user.ts
import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
export const userEntity = createEntity({
fields: {
email: createTextField({ required: true }),
displayName: createTextField({ required: true }),
phoneNumber: createTextField({
access: {
read: { roles: ["Admin", "Sysadmin"] },
write: { roles: ["Admin", "User"] },
},
}),
internalNotes: createTextField({
access: {
read: { roles: ["Admin"] },
write: { roles: ["Admin"] },
},
}),
},
});

Two independent rules per field, one for reads, one for writes. Read restricts who sees the value in list and detail responses; write restricts who can change it.

On reads, the pipeline strips fields the caller cannot see — the field simply doesn’t appear in the response. The list view, the detail view, the search index, and any custom query are all filtered. There is no extra branch in the handler.

On writes, the pipeline rejects payloads that try to set a forbidden field. The error is field_access_denied with the offending path (e.g. details.field: "internalNotes") — clients see exactly which field they cannot write.

The split between read and write matters. The phone-number example allows any user to write their own number (self-service) but only admins to read it (privacy). Two rules, one field, one source of truth.

  • Anonymous callers count as roles: ["anonymous"]. A field with read: { roles: ["User"] } is invisible to anonymous reads. If a field should appear on a public page, include "anonymous" in the read roles.
  • No declaration means visible. A field without an access block inherits the surrounding handler’s access — usually visible to whoever can call the handler. Write access: {} or omit the field altogether to enforce stricter rules.
  • Boot validation catches typos. A role name like "admin" (lowercase) when your feature declares "Admin" fails at boot, not at runtime.

Two fields on an employee entity demonstrating both axes — salary with split read/write rules, internalNotes admin-only on both sides:

export const employeeEntity = createEntity({
table: "read_sample_employees",
fields: {
name: createTextField({ required: true }),
email: createTextField({ required: true }),
// Salary: only Admin and Accounting can read, only Admin can write
salary: createNumberField({
access: { read: ["Admin", "Accounting"], write: ["Admin"] },
}),
// Internal notes: only Admin can read and write
internalNotes: createTextField({
access: { read: ["Admin"], write: ["Admin"] },
}),
},
});

Full source: samples/recipes/field-access.