Field-level access
Restrict individual fields by role — separate read and write rules per
field, no if branches in the handler. This recipe ships an employee
entity where salary is visible to Admin and Accounting (only Admin
writes) and internalNotes is Admin-only on both sides.
The framework strips fields the caller cannot read on the way out and rejects writes that touch fields the caller cannot write. Your handler never sees a forbidden field on input, never produces one on output.
What it shows
Section titled “What it shows”access: { read, write }on a field factory — two independent rules per field, both expressed as role lists.- Read-side filtering — querying as
Employeereturns the row without thesalaryandinternalNoteskeys; querying asAccountingreturnssalarybut notinternalNotes. - Write-side enforcement — a write payload that includes
internalNotesfrom a non-Admin role fails withfield_access_deniedand the offending field path. - No handler-level branching — the same
r.queryHandlerbody serves all three roles; the framework filters output per call.
Feature composition
Section titled “Feature composition”hr → employee entity with field-level access on salary + internalNotesSingle feature, no bundled dependencies — field access is declared on
the field factories inside createEntity.
- Admin creates an employee with all fields populated.
- Accounting reads detail → sees
salary, notinternalNotes. - Employee reads detail → sees
name/emailonly. - Accounting tries to write
salary→field_access_denied(read-only). - Employee updates
name→ succeeds (unrestricted field).
When to reach for it
Section titled “When to reach for it”Sensitive columns that a feature still needs to expose — phone numbers, salary figures, internal compliance notes. The whole row stays usable for everyone; specific fields disappear for callers without the right role.
bun kumiko test integration samples/field-accessOr from the recipe directory:
bun test src/__tests__/feature.integration.test.tsCovers all three roles reading and writing, including the expected
field_access_denied response shape.
Related samples
Section titled “Related samples”- basic-entity — start here if you need standard CRUD before adding field rules.
- custom-handlers — explicit handlers when generated CRUD is not enough.
Source code
Section titled “Source code”The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):
import { createEntity, createNumberField, createTextField, defineFeature, registerEntityCrud,} from "@cosmicdrift/kumiko-framework/engine";
export const employeeEntity = createEntity({ table: "read_sample_employees", fields: { name: createTextField({ required: true }), email: createTextField({ required: true }), salary: createNumberField({ access: { read: ["Admin", "Accounting"], write: ["Admin"] }, }), internalNotes: createTextField({ access: { read: ["Admin"], write: ["Admin"] }, }), },});
const allRoles = { access: { roles: ["Admin", "Accounting", "Employee"] } } as const;
export const employeeFeature = defineFeature("hr", (r) => { registerEntityCrud(r, "employee", employeeEntity, { write: allRoles, read: allRoles, verbs: { delete: false, list: false, restore: false }, });});📄 On GitHub: samples/recipes/field-access/src/feature.ts