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.
Prerequisites
Section titled “Prerequisites”- You’ve read Auth and permissions so the three-layer model is clear.
- Your feature already declares roles.
The code
Section titled “The code”Field-level access lives on the field definition itself:
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.
What the framework does
Section titled “What the framework does”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.
Common gotchas
Section titled “Common gotchas”- Anonymous callers count as
roles: ["anonymous"]. A field withread: { 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
accessblock inherits the surrounding handler’s access — usually visible to whoever can call the handler. Writeaccess: {}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.
Live example
Section titled “Live example”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.
See also
Section titled “See also”- Auth and permissions — the full three-layer model.
- Multi-tenancy — tenant scoping runs before any role check.