Add an access policy
Row filters, field masks, attribute checks. The full AccessModel surface explained.
Vadyl's access model is part of your product model — not a middleware you bolt on, not a library you wire up per route. It lives on the entity, gets compiled into native row-level security where the provider supports it, and is enforced uniformly across REST, GraphQL, gRPC, the SDK, and authored runtime.
The five facets
An AccessModel defines five canonical facets:
- readFilter — predicate a row must satisfy to be returned.
- insertCheck — predicate a row must satisfy on create.
- updateTargetFilter — predicate the existing row must satisfy to be eligible for update.
- updateResultCheck — predicate the new row must satisfy after update.
- deleteFilter — predicate the row must satisfy to be deleted.
access.model({
readFilter: access.where("ownerId", "==", ctx.userId),
insertCheck: access.where("ownerId", "==", ctx.userId),
updateTargetFilter: access.where("ownerId", "==", ctx.userId),
updateResultCheck: access.where("ownerId", "==", ctx.userId),
deleteFilter: access.deny(),
});Field-level rules
Beyond row-level checks, you can specify per-field read/write rules with masking:
access.model({
readFilter: access.allow(),
fieldRules: {
ssn: {
canRead: access.where("role", "in", ["owner", "admin"]),
maskWhen: access.deny(), // mask if cannot read
maskingMode: "redacted", // show "***" instead of null
},
notes: {
canWrite: access.where("role", "in", ["owner", "support"]),
},
},
});Masking modes: omit (drop the field), null (return null), redacted (return a placeholder string).
Attribute predicates
Predicates can reference the actor context — typed primitives that fail closed if not bound:
ctx.userId— canonical subject idctx.hasRole(role)— role membershipctx.hasClaim(name, value)— JWT-style claim checkctx.inContextSet(set)— context-set membership (org membership, project membership, etc.)ctx.contextValue(key)— opaque scoped value (tenant id, region, etc.)ctx.authStrengthAtLeast(level)— minimum auth strength gatectx.authenticatedVia(connector)— specific connectorctx.subjectTypeIs("user" | "agent" | "service")— actor type
Bypass conditions
Sometimes service code needs to operate above the access model — a background job reconciling state, a webhook handler the user didn't initiate. Use a typed bypass:
access.model({
// ...
bypass: {
fullBypass: access.where("subjectType", "==", "service")
.and(access.hasRole("system:reconciler")),
},
});Every bypass invocation is recorded on theAccessEnforcementDiagnostics.Bypasses audit trail with the entity, facet, actor, and reason — so you always know when the envelope was opened.
Native vs runtime enforcement
Vadyl's access engine consults the bound provider's SecurityCapabilities and decides per-facet:
- Native — predicate is compiled into RLS / row-level filters in the database.
- Runtime — predicate is enforced at the application layer via filter injection.
- Hybrid — partial native, runtime supplementation for unsupported clauses.
Either way, the canonical decision is the same — your product intent doesn't care which mode the provider supports.
Explainability
Every access decision carries a stable reason code. Use the explainability plane to see exactly why a read or write was allowed or denied:
vadyl explain access --entity Order --filter "status=paid" --as user:abc