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";| Export | Description |
|---|---|
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 |
PluginEngine | Plugin registration and lifecycle manager |
DebugEngine | Event 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 eventsArchitecture
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.