Backend & Node.js

react-access-engine exports pure engine functions — no React, no DOM, no context needed. Use the exact same access logic on your backend that you use in your React app.

One config. Frontend hooks + backend engine functions. Same logic everywhere.

Engine Functions

All engine functions are pure logic. Import them in any Node.js environment:

import {
  hasPermission,
  hasRole,
  hasAnyRole,
  hasAllRoles,
  getPermissionsForUser,
  hasAnyPermission,
  hasAllPermissions,
  evaluateFeature,
  evaluateAllFeatures,
  evaluatePolicy,
  assignExperiment,
  hasPlanAccess,
  getPlanTier,
  // Condition engine
  evaluateConditions,
  buildConditionContext,
  // Config helpers
  mergeConfigs,
  // Plugin system
  PluginEngine,
  DebugEngine,
} from "react-access-engine";
ExportDescription
hasRole(user, role)Check if a user has a specific role
hasAnyRole(user, roles)Check if a user has any of the given roles
hasAllRoles(user, roles)Check if a user has all of the given roles
getPermissionsForUser(user, config)Get all permissions resolved for a user
hasPermission(user, permission, config)Check a single permission (supports wildcards)
hasAnyPermission(user, permissions, config)Check if user has any of the permissions
hasAllPermissions(user, permissions, config)Check if user has all of the permissions
evaluateFeature(name, user, config, env?)Evaluate a feature flag — returns { enabled, reason }
evaluateAllFeatures(user, config, env?)Evaluate all feature flags — returns a Map
evaluatePolicy(permission, user, config, ctx?)Evaluate ABAC policy rules — returns { effect, matchedRule, reason }
assignExperiment(experiment, user)Deterministic A/B variant — returns { experimentId, variant }
hasPlanAccess(user, requiredPlan, config)Check if user's plan meets the tier requirement
getPlanTier(user, config)Get the user's plan tier index
evaluateConditions(conditions, context)Evaluate ABAC conditions against a context
buildConditionContext(user, resource?, env?)Build a condition context object
mergeConfigs(...configs)Merge multiple access configs together
PluginEnginePlugin registration and lifecycle manager
DebugEngineEvent recording for development and testing

Shared Config Pattern

Define your access rules once in a shared package and use them on both frontend and backend — no drift, no duplication:

// shared-config/src/index.ts
import type { AccessConfig } from "react-access-engine";

export const accessConfig: AccessConfig = {
  roles: {
    admin: { permissions: ["articles:*", "users:*", "settings:*"] },
    editor: {
      permissions: ["articles:read", "articles:write", "articles:publish"],
    },
    viewer: { permissions: ["articles:read"] },
  },
  features: {
    darkMode: { enabled: true },
    betaEditor: { enabled: true, roles: ["admin", "editor"] },
    newDashboard: { enabled: false },
  },
  plans: {
    order: ["free", "starter", "pro", "enterprise"],
    features: {
      analytics: "starter",
      apiAccess: "pro",
      customTheme: "enterprise",
    },
  },
  policies: {
    "articles:edit": {
      conditions: {
        "user.role": { operator: "in", value: ["admin", "editor"] },
      },
    },
    "articles:delete": {
      conditions: {
        "user.role": { operator: "eq", value: "admin" },
      },
    },
  },
  experiments: {
    checkoutFlow: {
      variants: ["control", "variantA", "variantB"],
      weights: [50, 25, 25],
    },
  },
};

React app — use hooks and components with the shared config:

import { AccessProvider } from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

<AccessProvider config={accessConfig} user={currentUser}>
  <App />
</AccessProvider>;

Express API — use engine functions with the same config:

import { hasPermission } from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

app.get("/api/articles", (req, res) => {
  if (!hasPermission(req.user, "articles:read", accessConfig)) {
    return res.status(403).json({ error: "Forbidden" });
  }
  // ...
});

Express Middleware

Permission Guards

Create reusable middleware for permission checks:

import { hasPermission } from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

function requirePermission(permission: string) {
  return (req, res, next) => {
    if (!hasPermission(req.user, permission, accessConfig)) {
      return res.status(403).json({
        error: "Forbidden",
        required: permission,
      });
    }
    next();
  };
}

// Usage
app.get("/api/articles", requirePermission("articles:read"), getArticles);
app.post("/api/articles", requirePermission("articles:write"), createArticle);
app.delete(
  "/api/articles",
  requirePermission("articles:delete"),
  deleteArticle,
);
app.put("/api/settings", requirePermission("settings:write"), updateSettings);

Feature-Gated Endpoints

import { evaluateFeature, evaluateAllFeatures } from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

app.get("/api/dashboard", (req, res) => {
  const result = evaluateFeature("newDashboard", req.user, accessConfig);

  if (result.enabled) {
    return res.json(getNewDashboardData());
  }
  return res.json(getLegacyDashboardData());
});

// Return all features at once
app.get("/api/features", (req, res) => {
  const allFeatures = evaluateAllFeatures(req.user, accessConfig);
  const features = Array.from(allFeatures.entries()).map(([name, result]) => ({
    name,
    enabled: result.enabled,
    reason: result.reason,
  }));
  res.json({ features });
});

Plan-Gated Endpoints

import { hasPlanAccess, getPlanTier } from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

app.get("/api/analytics", (req, res) => {
  if (!hasPlanAccess(req.user, "starter", accessConfig)) {
    return res.status(403).json({
      error: "Upgrade required",
      required: "starter",
      current: req.user.plan,
    });
  }
  res.json(getAnalyticsData());
});

app.get("/api/plan-info", (req, res) => {
  const tier = getPlanTier(req.user, accessConfig);
  res.json({
    plan: req.user.plan,
    tierIndex: tier,
    hasApiAccess: hasPlanAccess(req.user, "pro", accessConfig),
    hasAnalytics: hasPlanAccess(req.user, "starter", accessConfig),
  });
});

ABAC Policy Evaluation

import {
  evaluatePolicy,
  evaluateConditions,
  buildConditionContext,
} from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

app.put("/api/articles/:id", (req, res) => {
  const result = evaluatePolicy("articles:edit", req.user, accessConfig, {
    resource: { ownerId: article.authorId },
  });

  if (result.effect === "deny") {
    return res
      .status(403)
      .json({ error: "Policy denied", reason: result.reason });
  }
  // proceed with edit...
});

// Low-level condition evaluation
app.post("/api/evaluate", (req, res) => {
  const context = buildConditionContext(req.user, req.body.resource, {
    time: new Date().toISOString(),
  });
  const result = evaluateConditions(req.body.conditions, context);
  res.json({ allowed: result });
});

A/B Experiments on the Backend

import { assignExperiment } from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

app.get("/api/checkout", (req, res) => {
  const experiment = accessConfig.experiments?.checkoutFlow;
  if (!experiment) return res.json({ variant: "control" });

  const assignment = assignExperiment(experiment, req.user);
  // Deterministic — same user always gets the same variant

  switch (assignment.variant) {
    case "variantA":
      return res.json({ layout: "single-page", variant: assignment.variant });
    case "variantB":
      return res.json({ layout: "multi-step", variant: assignment.variant });
    default:
      return res.json({ layout: "classic", variant: "control" });
  }
});

Full User Access Snapshot

Return everything a user can do in a single API call:

import {
  hasRole,
  getPermissionsForUser,
  evaluateAllFeatures,
  hasPlanAccess,
  getPlanTier,
  assignExperiment,
} from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

app.get("/api/access/snapshot", (req, res) => {
  const user = req.user;

  const allFeatures = evaluateAllFeatures(user, accessConfig);
  const features = Array.from(allFeatures.entries()).map(([name, result]) => ({
    name,
    enabled: result.enabled,
    reason: result.reason,
  }));

  res.json({
    roles: {
      isAdmin: hasRole(user, "admin"),
      isEditor: hasRole(user, "editor"),
      isViewer: hasRole(user, "viewer"),
    },
    permissions: [...getPermissionsForUser(user, accessConfig)],
    features,
    plan: {
      current: user.plan,
      tier: getPlanTier(user, accessConfig),
      hasAnalytics: hasPlanAccess(user, "starter", accessConfig),
      hasApiAccess: hasPlanAccess(user, "pro", accessConfig),
    },
    experiments: {
      checkoutFlow: assignExperiment(
        accessConfig.experiments?.checkoutFlow,
        user,
      ),
    },
  });
});

Plugins on the Backend

Plugins work identically on frontend and backend. Use them for audit logging, analytics, and custom ABAC operators:

import {
  PluginEngine,
  DebugEngine,
  createAuditLoggerPlugin,
  createAnalyticsPlugin,
  createOperatorPlugin,
  hasPermission,
} from "react-access-engine";
import { accessConfig } from "@myapp/shared-config";

// Audit logging — track every authorization decision
const auditPlugin = createAuditLoggerPlugin((event) => {
  console.log(`[AUDIT] ${event.action}: ${event.result ? "ALLOW" : "DENY"}`, {
    userId: event.userId,
    timestamp: event.timestamp,
  });
});

// Analytics — track feature & experiment usage
const analyticsPlugin = createAnalyticsPlugin((event) => {
  trackEvent("access_check", event);
});

// Custom ABAC operators
const operatorPlugin = createOperatorPlugin({
  withinGeo: (actual, expected) => expected.includes(actual),
  matchesTier: (actual, expected) => {
    const tiers = ["free", "starter", "pro", "enterprise"];
    return tiers.indexOf(actual) >= tiers.indexOf(expected);
  },
});

// Register all plugins
const pluginEngine = new PluginEngine();
pluginEngine.registerAll([auditPlugin, analyticsPlugin, operatorPlugin]);

// Use in request handling — emit events for audit/analytics
app.get("/api/articles", (req, res) => {
  const allowed = hasPermission(req.user, "articles:read", accessConfig);

  pluginEngine.emitAccessCheck({
    permission: "articles:read",
    granted: allowed,
    roles: [...req.user.roles],
    timestamp: Date.now(),
  });

  if (!allowed) return res.status(403).json({ error: "Forbidden" });
  // ...
});

Debug Engine

Use DebugEngine for development and testing:

const debugEngine = new DebugEngine();

debugEngine.recordEvent({
  type: "permission",
  key: "articles:read",
  result: allowed,
  user,
  timestamp: Date.now(),
});

console.log(debugEngine.getEvents()); // All recorded events

Architecture

react-access-engine/
├── React Layer (hooks, components, context)
│   └── useAccess, usePermission, useRole, useFeature, …
│   └── <AccessProvider>, <Can>, <Feature>, <Allow>, …
│
├── Engine Functions (pure logic, no React)
│   └── hasPermission, hasRole, evaluateFeature, …
│   └── evaluatePolicy, assignExperiment, hasPlanAccess, …
│
└── Plugin System (works everywhere)
    └── PluginEngine, DebugEngine, audit, analytics, …

One install. One import path. Frontend and backend.