Why Toride
Toride is a relation-aware authorization engine for TypeScript. You define your entire authorization model in YAML, generate type-safe bindings, and plug in any data source — no database required.
YAML as the Single Source of Truth
Your authorization model lives in a single YAML file. Roles, permissions, relations, derived roles, and conditional rules are all declared in one place — not scattered across middleware, decorators, or application code.
# policy.yaml — the complete authorization model
version: "1"
actors:
User:
attributes:
id: string
department: string
isSuperAdmin: boolean
global_roles:
superadmin:
actor_type: User
when:
$actor.isSuperAdmin: true
resources:
Project:
roles: [viewer, editor, admin]
permissions: [read, update, delete]
grants:
viewer: [read]
editor: [read, update]
admin: [all]
derived_roles:
- role: admin
from_global_role: superadmin
- role: editor
actor_type: User
when:
$actor.department: $resource.departmentEvery authorization decision the engine makes traces back to this file. When you need to audit who can do what, you read the YAML — not the codebase.
Type Safety via Codegen
@toride/codegen reads your policy file and generates TypeScript types for every resource, role, permission, and relation. Typos and mismatches are caught at compile time, not in production.
npx toride-codegen policy.yaml -o src/generated/policy-types.tsThe generated output gives you typed resource names, role maps, and resolver signatures:
// Auto-generated by @toride/codegen — do not edit
export type Resources = "Organization" | "Project" | "Task";
export interface RoleMap {
Organization: "member" | "admin";
Project: "viewer" | "editor" | "admin";
Task: "viewer" | "editor";
}
export interface PermissionMap {
Project: "read" | "update" | "delete";
Task: "read" | "update" | "delete";
}
export interface RelationMap {
Project: { org: "Organization" };
Task: { project: "Project"; assignee: "User" };
}If you rename a role in your YAML and forget to update a resolver, the TypeScript compiler tells you immediately.
Database-Agnostic by Design
Resolvers are functions. They take a resource reference and return attributes. Where the data comes from is entirely up to you — in-memory objects, a REST API, a GraphQL endpoint, a file, or a database.
// Plain objects — no database, no ORM, no infrastructure
const projects: Record<string, any> = {
"proj-1": { status: "active", department: "engineering" },
};
const engine = new Toride({
policy: await loadYaml("./policy.yaml"),
resolvers: {
Project: async (ref) => {
const project = projects[ref.id];
return {
status: project.status,
department: project.department,
};
},
},
});The engine never assumes a database exists. It calls your resolver, gets attributes back, and evaluates the policy. The data source is your choice.
Relation-Based Policies
YAML policies can model deep resource hierarchies. Roles propagate automatically through relations — a user who is an admin of an Organization inherits that role on its Projects, and those Projects' Tasks, without any imperative code.
resources:
Organization:
roles: [member, admin]
permissions: [read, update, manage_projects]
grants:
member: [read]
admin: [all]
Project:
roles: [viewer, editor, admin]
permissions: [read, update, delete, create_task]
relations:
org: Organization
grants:
viewer: [read]
editor: [read, update, create_task]
admin: [all]
derived_roles:
# Organization admins are Project admins
- role: admin
from_role: admin
on_relation: org
# Organization members can view Projects
- role: viewer
from_role: member
on_relation: org
Task:
roles: [viewer, editor]
permissions: [read, update, delete]
relations:
project: Project
assignee: User
grants:
viewer: [read]
editor: [read, update, delete]
derived_roles:
# Project editors are Task editors
- role: editor
from_role: editor
on_relation: project
# Project viewers are Task viewers
- role: viewer
from_role: viewer
on_relation: project
# Task assignee is an editor
- role: editor
from_relation: assignee
rules:
- effect: forbid
permissions: [update, delete]
when:
resource.project.status: completedThis models a three-level hierarchy: Organization → Project → Task. An Organization admin automatically gets admin on every Project in that org, and editor on every Task within those Projects — all declared in YAML. The engine traverses the relation graph at evaluation time.
Partial Evaluation
When your data source is a database, you often need to answer "which resources can this user access?" without loading every record. buildConstraints() evaluates the policy partially — using only the actor's attributes — and produces a constraint AST that you translate into a WHERE clause.
const result = await engine.buildConstraints(actor, "read", "Project");
if ("forbidden" in result) {
return []; // no access
}
if ("unrestricted" in result) {
return await db.project.findMany(); // full access
}
// Translate constraints to a database query
const where = adapter.translate(result.constraints);
return await db.project.findMany({ where });For an actor with department: "engineering", the constraint AST might reduce to:
{
"type": "or",
"children": [
{ "type": "field_eq", "field": "department", "value": "engineering" },
{ "type": "always" }
]
}Actor attributes are inlined during evaluation — only resource-level conditions remain in the output. Toride ships adapters for Prisma and Drizzle, or you can implement the ConstraintAdapter interface for any query language.