Field-Level Access Control (FLAC)
Field-level security in SveltyCMS — what is enforced today (filter/search + API read filtering) and the schema-level utilities built for write guards.
On this page
SveltyCMS implements field-level security in two layers, plus optional
at-rest encryption for designated fields (encrypt: true — see
Field-Level Encryption):
- Schema-based FLAC utilities (
src/utils/field-access.ts):canAccessField/enforceFieldAccessevaluate per-fieldpermissions(visibility,requiredAuth,readRoles,writeRoles) declared in the Collection Builder. FIELD_PERMISSIONSresponse filter (August 2026): a cached settings-level policy ({collection: {role: [readable fields]}}) applied to API read responses inhandle-token-resolution.
This page describes both, and is explicit about what is enforced in the request path today versus what is built but not yet wired.
🛡️ Enforcement Status (Truthful)
| Layer | Status | Where |
|---|---|---|
| Filter/search whitelist — non-readers cannot filter (or search) restricted fields | ✅ Enforced | collection-filter-engine.ts uses canAccessField per field schema |
| SDK filter and sort — a guarded name is removed unless that caller may read it | ✅ Enforced | src/utils/guarded-query.ts. Admins, system callers, and unguarded schemas keep the same object. A keyset cursor on a blocked field is 400. |
| API read responses — fields filtered per role policy with pre-compiled Sets | ✅ Enforced (Zero-Allocation) | FIELD_PERMISSIONS setting applied in handle-token-resolution + field-permission-service.ts |
UI configuration — per-field readRoles / writeRoles / visibility / requiredAuth |
✅ Enforced | fieldDeclaresGuard + canAccessField. Private with an empty role list is admin-only. |
| Schema read redaction — private / role-restricted fields removed on SDK reads | ✅ Enforced | presentRead in collections-namespace.ts after the cache read. The cached row stays complete. |
Settings read filter (FIELD_PERMISSIONS) |
✅ Enforced when configured | handle-token-resolution. Point-read GET/HEAD skips that hook; schema redaction still runs. |
Write rejection — 403 + UNAUTHORIZED_ACCESS audit on guarded-field writes |
✅ Enforced | assertWriteAllowed in the SDK mutation pipeline and API handlers |
Parity status (September 2026, query strip October 2026): Schema visibility and role lists are enforced on SDK reads and on writes. FIELD_PERMISSIONS remains a separate settings filter. Unguarded collections skip both checks. The warm read lane declines a non-admin when any field is guarded, so it never caches a body that still contains a private field for that caller. SDK find, count, and streaming, plus the admin list loader, also drop a guarded filter or sort key. That strip copies the query only when a key is removed.
🏗️ Architecture
⚙️ Configuration
Per-field schema policy (Collection Builder)
FLAC is configured per-field in the Collection Builder under the Permissions tab.
// Technical Representation in Schema
{
db_fieldName: "internal_notes",
widget: widgets.Textarea,
permissions: {
visibility: "private",
requiredAuth: true,
readRoles: ["admin", "editor"],
writeRoles: ["admin"]
}
}
Settings-level response policy (FIELD_PERMISSIONS)
Admins may also define a collection → role → readable-fields map via System Settings. When present, it filters API read responses in handle-token-resolution:
{
"posts": {
"editor": ["title", "body", "status"],
"contributor": ["title"],
},
}
The memo is invalidated on settings save; admin and absent-policy requests skip the filter entirely (zero hot-path cost).
🛡️ Security Guarantees
1. Filter / Search Protection (enforced)
canAccessField is invoked by collection-filter-engine.ts before any query is built. Fields the caller’s role cannot read are excluded from where / whereBetween / search clauses — restricted fields cannot be used to probe data through filtering or full-text search.
2. Read Response Filtering (enforced)
Two independent filters:
- Schema.
presentReadcallsredactReadEnvelopeonfind,findById,findByIds, search, and collection streams. A private field and itsfield_desiblings are removed for anyone who is not an admin. The cache stores the full row. - Settings.
FIELD_PERMISSIONSfilters responses inhandle-token-resolutionwhen that setting exists. Point-read GET/HEAD skips that hook. Schema redaction still applies.
3. Write Rejection (enforced)
assertWriteAllowed runs for non-admins when fieldDeclaresGuard is true. It collects every denied field, writes an UNAUTHORIZED_ACCESS audit event, and throws 403 before the transaction.
4. Fail-Closed Default
permissions.visibility: "private" with an empty role list is admin-only on read and on write. A role listed in readRoles or writeRoles is allowed for that operation. requiredAuth denies anonymous callers. A public field with an empty role list is not a guard: the fast path stays on, which matches the Permissions tab (“empty selection follows visibility”). canAccessField still denies an empty role array when a caller asks about that field directly, which is how filters behave.
📊 Performance Impact
- O(N) sanitization — a single pass over the field schema.
- Zero-cost fast-path — admin and
systemusers bypass FLAC entirely;FIELD_PERMISSIONSfast-paths skip the filter when no policy matches. - Memoized policy — the
FIELD_PERMISSIONSmap is cached and only invalidated on settings save.
📚 Related Documentation
- Security Overview
- Role-Based Access Control (RBAC)
- Collection Filtering Platform
- 2026 Roadmap — Field-level write guards