Warning
This is still in development and has no stable API.
A tiny, type-safe authorization framework for Node.js applications. Composable. Extensible. Performant.
pnpm add @savvycodes/policyCreate a policy builder that knows about the current user (or whatever context type you need to validate your policy). Every call to the builder returns a strongly typed policy function for a specific resource.
// app/policies/base.ts
import { createPolicy } from "@savvycodes/policy";
export type User = { id: string; scopes: string[] };
export const policy = createPolicy<User>();
// app/policies/project.ts
import type { Project } from "./types";
import { policy } from "./base";
export const isOwner = policy<Project>(async (user, project) => {
return project.ownerId === user.id;
});
export const isProjectLead = policy<Project>((user, project) => {
return project.leadId === user.id;
});
export const isStakeholder = policy<Project>((user, project) => {
return project.stakeholderIds.includes(user.id);
});
// app/routes/project.ts
import { enforce } from "@savvycodes/policy";
import { isOwner } from "@/app/policies/project";
export async function loader() {
const { user } = await fakedb.getUser();
const project = await fakedb.getProject();
await enforce(isOwner, user, project);
}Combine policies with and and or. Unbound policies share the same resource; pre-bind a resource with .with when you need to mix different resource types.
import { enforce, or, and } from "@savvycodes/policy";
import { isOwner, isProjectLead, isStakeholder } from "@/app/policies/project";
export async function loader() {
const { user } = await fakedb.getUser();
const project = await fakedb.getProject();
await enforce(and(isOwner, isProjectLead), user, project);
await enforce(or(isOwner, isProjectLead), user, project);
// Deep compositions work as expected
await enforce(or(and(isOwner, isProjectLead), isStakeholder), user, project);
}Policies expose a .with(resource) (alias .bind(resource)) helper that returns a bound policy. This lets you combine different resource types inside a single logical expression while keeping the type system honest.
const isProjectLead = policy<Project>(
(user, project) => project.leadId === user.id
);
const isTeamOwner = policy<Team>((user, team) => team.ownerId === user.id);
const composite = and(isProjectLead.with(project), isTeamOwner.with(team));
await enforce(composite, user);Use a before hook to short-circuit policy evaluation. Returning true authorizes immediately, false denies, and null (or undefined) falls through to the original handler.
export const policy = createPolicy<User>({
async before(user) {
if (user.scopes.includes("superuser")) return true;
return null;
},
});enforce throws an AuthorizationError on failure. Provide a custom message or error instance if you need bespoke behaviour.
await enforce(isOwner, user, project, { message: "Forbidden" });
await enforce(isOwner, user, project, {
error: () => new HttpError(403, "Forbidden"),
});pnpm install
pnpm test
pnpm lint
pnpm build- Provide metrics
- Allow custom loggers (pino)
- Explore caching API (memory, valkey?, sqlite?)
- Integration with existing caching libs
- Separate package:
@savvycodes/policy/express - Explore middleware ergonomics
- Offer helpers for route-based enforcement
- Separate package:
@savvycodes/policy/remix - Explore loader/action helpers
- Provide declarative APIs for nested routes