Fields: masking, writable, computed
The fields config controls, per column, what clients can read, write, filter, and sort by. Combined with computed, generatedFields, and strictInput, it gives you defense against mass-assignment and over-exposure with no extra code.
Pass the Drizzle column (like id); a string column name also works but is deprecated.
useResource(usersTable, {
id: usersTable.id,
db,
fields: {
readable: [usersTable.id, usersTable.name, usersTable.email, usersTable.createdAt],
writable: [usersTable.name, usersTable.email],
filterable: [usersTable.name, usersTable.email, usersTable.createdAt],
sortable: [usersTable.name, usersTable.createdAt],
},
});
Read masking — fields.readable
When set, readable is an allowlist of table columns that may leave the server. Any column not listed is stripped from every response — list, get, create, update, batch, search — and from every subscription event (existing, added, changed) and the initial snapshot.
The mask is applied server-side, so a client cannot recover a hidden column via ?select= or by subscribing:
fields: {
// passwordHash, internalNotes, etc. are never returned, regardless of ?select=
readable: [usersTable.id, usersTable.name, usersTable.email, usersTable.createdAt],
}
Only table columns are masked. Relation keys (loaded via ?include=), computed values, and internal markers like _etag/_optimisticId always pass through, so includes keep working. See the auth contract for the guarantee.
Write enforcement — fields.writable (mass-assignment protection)
When set, writable is an enforced allowlist of table columns a client may set on create/update. Any table column not listed is silently stripped from the incoming body before it reaches lifecycle hooks or the database — on POST /, PATCH /:id, PUT /:id, POST /batch, PATCH /batch, and POST /batch/upsert.
This stops a malicious client from setting protected columns (e.g. role, isAdmin, ownerId) by smuggling them into a body.
Exemptions:
- The primary key (
id) is never stripped. - Columns in
generatedFieldsare never stripped. - Non-column keys (relation payloads for nested writes, etc.) always pass through — only real table columns are subject to the allowlist.
Stripping happens before hooks run, so a server-side onBeforeCreate/onBeforeUpdate can still set a protected field itself:
hooks: {
onBeforeCreate: async (ctx, data) => ({ ...data, ownerId: ctx.user.id }), // ownerId set server-side
}
See Secure queries for the broader pattern.
Filter & sort allowlists
fields.filterable— only these columns may appear in?filter=; others return a400 FilterParseError.fields.sortable— only these columns may appear in?orderBy=.
Use these to prevent clients from filtering/sorting on sensitive or unindexed columns.
strictInput
By default, unknown fields in a body are silently ignored. Set strictInput: true to reject them with a 422 (Zod strict mode):
{ strictInput: true }
generatedFields may still be omitted. Combine strictInput (reject unknown fields) with fields.writable (strip known-but-not-writable columns) for the strictest input handling.
Generated fields
generatedFields marks columns the server fills in (id, timestamps, ownership). They are:
- exempt from
fields.writablestripping (a hook can set them), - optional in inbound bodies even under
strictInput.
{ generatedFields: [posts.id, posts.userId, posts.createdAt, posts.updatedAt] }
Computed fields
Virtual fields added to every response and subscription event, computed from the full, unmasked row and never persisted:
computed: {
fullName: (row) => `${row.firstName} ${row.lastName}`,
isOverdue: (row) => row.dueAt != null && Date.parse(row.dueAt) < Date.now(),
}
Computed fields:
- appear in list, get, create, update, batch, and search responses, plus every subscription event and the initial snapshot;
- can derive from columns that
fields.readablehides (they read the unmasked row); - are exempt from read masking (not table columns), so the
readableallowlist never strips them; - are not written to the database.