most "type-safe" permission checks are only as trustworthy as data typescript never actually verified
i'm the author of zap-studio/permit, a small authorization library. i'm posting this because of a specific problem that i feel i'm either totally right or totally wrong.
types don't exist at runtime. that's not news, but it's easy to forget it applies to permission checks specifically. a check like ctx.user.id === post.authorId "type-checks" fine. but post usually came from a db row, an api payload, or whatever. none of that passed through a place typescript could verify it. if a lazy join returns authorId: null, or a migration changes a column's nullability, the check still compiles. it just quietly does the wrong thing at runtime, which is the one place a permission check actually matters.
most type-safe permission libraries stop at the type. permit doesn't: resources are defined with a standard schema validator (zod, valibot, or arktype), and policy.can() re-validates the resource against that schema every time it evaluates a rule, not just once at setup. an invalid resource resolves to false. meaning, it fails closed, it never throws.
here's a code example
const policy = createPolicy<AppContext>({
resources,
actions,
rules: {
post: {
read: allow(),
write: when((ctx, _, post) => ctx.user?.id === post.authorId),
delete: deny(),
},
},
});
await policy.can(context, "post:read", postResource);
conditions compose with and(), or(), not(), and whole policies compose with mergePoliciesAnd / mergePoliciesOr, so you can split authorization by feature or team and combine it later.
but, if you already know permix, which is the most established option here, it seems it doesn't solve this specific issue, mostly because it's deny by default (like permit). nothing stops write: (ctx, post) => ctx.user.id === post.authorId from running against bad data and returning true, undefined === undefined is true, so a missing authorId can accidentally grant access nobody meant to grant. permit isn't "better," it just refuses to run the rule at all if the resource fails its schema first. curious if others have hit this in practice or think it's rare enough not to matter.
and here are some links for curious people to check the code or docs:
- repo: github.com/zap-studio/monorepo (packages/permit)
- docs: zapstudio.dev/permit