Field-Level Security
Control visibility and editability of specific fields — readable/editable rules, hidden vs. read-only semantics, and server-side enforcement of declared rules.
Field-Level Security
Control visibility and editability of specific fields.
Field Permissions in Permission Sets
Field permissions are keyed <object>.<field> and use readable / editable:
fields: {
// Read-only: visible but not editable
'account.annual_revenue': { readable: true, editable: false },
'account.description': { readable: true, editable: true },
// Hidden: not visible at all
'account.ssn': { readable: false, editable: false },
'opportunity.amount': { readable: true, editable: true },
'opportunity.probability': { readable: true, editable: false },
}Hidden vs. Read-Only
// Hidden: Field not visible at all
{ readable: false, editable: false }
// Read-Only: Field visible but not editable
{ readable: true, editable: false }
// Editable: Field visible and editable
{ readable: true, editable: true }Server-side enforcement of declared rules
The client-side ObjectForm / inline grid hides non-editable fields from the UI — but that is a UX layer only. The SecurityPlugin middleware enforces field-level write rules on the server, regardless of how the request arrived (REST, GraphQL, raw ObjectQL call).
On read — find / findOne results have non-readable fields
stripped from every record before the response leaves the engine.
On write — insert / update requests are checked before the
operation reaches the driver. If the payload contains any field the
caller is not permitted to edit, the engine throws
PermissionDeniedError (mapped to HTTP 403) with the offending field
names in details.forbiddenFields:
{
"error": {
"code": "PERMISSION_DENIED",
"message": "[Security] Field write denied: not permitted to edit [salary, ssn] on 'employee'",
"details": {
"operation": "insert",
"object": "employee",
"forbiddenFields": ["salary", "ssn"]
}
}
}Why throw instead of silently stripping? Silent strip hides the security boundary from honest clients (their update "doesn't save" and they cannot tell why) AND gives a probing client no signal that the field exists. Throwing makes the boundary observable in both directions — legitimate UIs get an actionable error to fix; probing clients learn nothing they could not already infer.
Default-visible (block-list) semantics. Fields without an explicit
rule pass through untouched — readable and writable. Permission sets
only constrain fields they explicitly enumerate, and field grants union
most-permissively across a user's sets (one set's readable: true
out-votes another set's false). Until a subtractive muting layer lands
(ADR-0066 ⑧), a { readable: false } rule masks the field only as long
as no other set the user holds declares it readable: true — so
protect sensitive fields by granting them only in the sets that need
them, and treat a sensitive field on a broadly-granted object as a
review smell — see the
FLS posture warning.
Declared rules themselves are enforced fail-closed: a masked field is
stripped on read and a write to a non-editable field throws.
Bulk inserts. Arrays are checked row-by-row; a single offending field in any row rejects the whole batch atomically.
System operations. ExecutionContext { isSystem: true } bypasses
the check entirely — used for migrations, seed loading, and audit log
writes.