AuthRailAuthRail

Core Concepts

How AuthRail evaluates middleware.


AuthRail is built on three pillars: Rails, Context, and Decisions. Understanding these is key to mastering the system.


1. The Rail

A Rail is an immutable pipeline of authorization checks. It represents a single specific "path" of permission logic (e.g., "Admin Rail", "Billing Rail", "Public API Rail").

Think of it like a security checkpoint at an airport: Each person (context) must pass through a sequence of scanners (middleware). If any scanner detects an issue, the person is either turned away or redirected to a secondary check.

Creating a Rail
import { createRail } from "authrail";

export const billingRail = createRail("billing", [
  requireAuth("/login"),
  requireSubscription(),
  allowIf((ctx) => ctx.user.canManageBilling),
]);

Key Characteristics:

  • Sequential: Middleware is executed strictly in the order provided in the array.
  • Micro-evaluation: Execution stops the moment a deny or redirect is encountered.
  • Traceable: With debug: true, you can observe exactly where a context was halted.

2. The Context

The Context is the "data envelope" that carries all the information needed to make a decision. AuthRail is unique because it doesn't force a specific context structure—you define it.

Defining Context
type AppContext = {
  user: User | null;         // Authentication state
  resource?: Document;       // The thing being accessed
  environment: "dev" | "prod"; // System state
  ipAddress: string;         // Request metadata
};

Immutability & Enrichment

While the initial context you provide is treated as read-only by most middleware, AuthRail supports context enrichment. A middleware can return a context object which will be shallow-merged with the original context for all subsequent middleware in the rail.

This is powerful for patterns like:

  • Fetching user permissions once and attaching them to the context.
  • Resolving a resource ID into a full object for later checks.

3. The Decision

The Decision is the final verdict of a rail evaluation. Unlike boolean-based systems, AuthRail provides structured intent.

Decision Types
type Decision = 
  | { type: "allow" }                     // Everything passed
  | { type: "deny", message?: string }     // Blocked locally
  | { type: "redirect", to: string };      // Instruction to move elsewhere

Result Object

When you call rail.evaluate(context), you receive a Result object:

Evaluation Result
const { decision, context, name } = await adminRail.evaluate(ctx);
  • decision: The final result (allow, deny, or redirect).
  • context: The final enriched context (including any data added by middleware).
  • name: The name of the rail (useful for logging and telemetry).

Execution Dynamics

1. Strict Sequential Order

Middleware runs strictly in the order you define. There is no parallel execution, and there are no reorderings. This ensures that a requireAuth check always runs before a requireRole check, preventing "undefined" errors when accessing ctx.user.role.

2. Micro-Evaluation (Fast-Fail)

The moment a middleware returns a deny or redirect, the rail stops. No further middleware functions are called. This saves resources and ensures that potentially expensive permission checks aren't performed if the user is already blocked.

3. Default Allow

If the context makes it to the end of the rail without any middleware returning a blocking decision, the rail returns allow. This follows the principle of "composition of constraints"—you list what checks must pass, and if they all do, access is granted.


The Big Picture: How they interact

  1. Define a Rail with a set of Middleware.
  2. Assemble a Context based on the current request or user session.
  3. Evaluate the Rail with that Context.
  4. Execute your application logic based on the resulting Decision.

By keeping these concerns separate, AuthRail ensures that your authorization logic remains testable, reusable, and perfectly predictable.

On this page