Skip to content

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.

yaml
# 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.department

Every 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.

bash
npx toride-codegen policy.yaml -o src/generated/policy-types.ts

The generated output gives you typed resource names, role maps, and resolver signatures:

typescript
// 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.

typescript
// 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.

yaml
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: completed

This 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.

typescript
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:

json
{
  "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.