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, personal: "self", find: "exact" }), displayName: createTextField({ required: true, personal: "self", find: "fuzzy" }), phoneNumber: createTextField({ personal: "self", find: "none", access: { read: ["Admin", "SystemAdmin"], write: ["Admin", "User"], }, }), internalNotes: createTextField({ personal: "self", find: "none", access: { read: ["Admin"], write: ["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, so the field does not appear in the response. Entity-bound list and detail results are filtered after the handler returns. A custom query hook must apply the same rule to fields it adds itself.
On writes, the pipeline rejects payloads that try to set a forbidden
field. The access-denied error carries details.reason: "field_access_denied"
and the field name (for example details.field: "internalNotes"), so clients
see exactly which field they cannot write.
The split between read and write matters. A caller can write a field only when the write role rule permits it and, for ownership rules, both the old and new row match. Two rules, one field, one source of truth.

Common gotchas
Section titled “Common gotchas”- Anonymous callers count as
roles: ["anonymous"]. A field withread: ["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 is available to callers who can use the surrounding handler. There is no usefulaccess: {}deny-all shorthand; use explicit role or ownership rules. - Boot validation catches typos. Kumiko knows a role when a handler
access rule uses it. A field role like
"admin"(lowercase), when your handlers only use"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({ personal: { of: "id" }, find: "none", required: true }), email: createTextField({ personal: { of: "id" }, find: "none", required: true }), salary: createNumberField({ access: { read: ["Admin", "Accounting"], write: ["Admin"] }, }), internalNotes: createTextField({ personal: { of: "id" }, find: "none", 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.