Clarifications & Constraints
Technical boundaries, philosphy, and explicit constraints.
To keep AuthRail fast, predictable, and maintainable, we've made explicit architectural choices. Understanding these boundaries will help you integrate the library effectively.
1. Not an Authentication System
Constraint: AuthRail does not handle user sessions, login flows, or password hashing.
Rationale: Authentication is a complex, solved problem with many great providers (Clerk, Auth0, Supabase). AuthRail starts after the user is identified. It answers the question: "Now that I know who this user is, what are they allowed to do?" This separation of concerns allows AuthRail to remain lightweight and framework-agnostic.
2. Not a Router
Constraint: Evaluation does not automatically trigger window navigation or history state changes.
Rationale: Every framework (Next.js, Remix, React Router) handles navigation differently. By emitting a structured redirect decision instead of performing the redirect itself, AuthRail gives you full control over how to handle transitions, ensuring compatibility with Server Components, Edge Middleware, and SPAs.
3. No Global State or Side Effects
Constraint: Individual evaluate() calls are independent. There is no internal caching or shared memory between rails.
Rationale: Side effects make authorization logic hard to test and debug. By treating the evaluation as a pure function of (Rail, Context) => Decision, we ensure that your permission logic is 100% deterministic. If a check fails, it's because of the data in the context, not an invisible "stale" cache.
4. Sequential (Non-Parallel) Execution
Constraint: Middleware runs one by one in array order.
Rationale: Authorization often depends on a logical hierarchy. You shouldn't check a user's role before you know the user exists. Sequential execution prevents "race conditions" within your permission logic and simplifies error handling. If a check is expensive (like a DB call), it will only run if all preceding "cheap" checks have already passed.
5. Immutable Context (Read-Only)
Constraint: Middleware should never directly mutate the context object.
Rationale: TypeScript enforces Readonly<Ctx> within middleware functions. Mutating shared objects in a pipeline leads to hard-to-track bugs. If you need to add data to the context, use the Enrichment Pattern:
// DO THIS: Return new data to be merged
return {
context: { userData: await fetchUser() }
};
// DON'T DO THIS:
// ctx.userData = await fetchUser();6. Shallow Context Merging
Constraint: When middleware returns a context object, it is shallow-merged ({...old, ...new}) with the working context.
Rationale: Deep merging is computationally expensive and can lead to unexpected results with nested arrays or objects. Keeping merging shallow forces a cleaner, flatter context structure that is easier to type and reason about.