@lbstack/accessx
v0.3.1
Published
A role & resource based access control system with end to end type safety.
Downloads
46
Readme
@lbstack/accessx
A TypeScript-first RBAC permission engine with automatic permission generation, designed to be the single source of truth for:
- Backend authorization
- Frontend UI access control
- Database permission storage
- Admin permission management
No manual permission strings.
No role leakage to frontend.
Full autocomplete everywhere.
✨ Key Features
- 🔐 Automatic permission generation
- 🧠 Strong TypeScript inference & autocomplete
- 🏗️ Single initialization – use everywhere
- 🖥️ Backend (Express / Nest / Hono)
- 🎨 Frontend (React hooks & components)
- 🗄️ Database-friendly permission keys
- 📦 Package & framework agnostic
🧩 Core Concept
You define:
- Roles
- Actions
- Resources (modules)
The package automatically generates permissions in this format:
RESOURCE_KEY:ACTION
Example:
BLOGS:CREATE BLOGS:READ USER:DELETE
These permission keys are:
- Stored in DB
- Sent to frontend after login
- Used in UI & API checks
- Fully type-safe
📦 Installation
npm install @lbstack/accessx
or
pnpm add @lbstack/accessx
🚀 Initialization (Single Source of Truth)
import { createAccess } from "@lbstack/accessx";
export const access = createAccess({
roles: ["ADMIN", "EDITOR", "CUSTOMER"] as const,
actions: ["CREATE", "READ", "UPDATE", "DELETE"] as const,
resources: [
{
name: "Users",
key: "USER",
description: "User management",
},
{
name: "Blogs",
key: "BLOGS",
description: "Blog posts",
},
] as const,
});
⚠️ as const is required for TypeScript autocomplete.
🔑 Automatically Generated Permissions
access.permissionKeys
[
"USER:CREATE",
"USER:READ",
"USER:UPDATE",
"USER:DELETE",
"BLOGS:CREATE",
"BLOGS:READ",
"BLOGS:UPDATE",
"BLOGS:DELETE",
]
No permission strings are written manually.
🗄️ Database Usage
Seed permissions table
await db.permissions.insertMany(access.permissions);
Each permission contains metadata:
{
key: "BLOGS:CREATE",
resource: { name, key, description },
action: "CREATE"
}
🔐 Backend Usage
Assign permissions to roles
access.allow("ADMIN", "USER:DELETE");
access.allow("EDITOR", "BLOGS:CREATE");
Check permission (Service / Controller)
access.can(user.role, "BLOGS:UPDATE");
Normalize permissions from DB
const permissionsFromDb = ["BLOGS:READ", "USER:DELETE"];
const permissions = access.normalizePermissions(permissionsFromDb);
Ensures only valid generated permissions are used.
res.json({
user,
permissions: access.resolvePermissions(user.role),
});🔑 Assigning Permissions
There are two ways to assign permissions to roles: Manual (Static) and Dynamic (Database-linked).
1. Manual Assignment (Static)
Best for hardcoded defaults or simple applications. This is the standard, non-mandatory approach.
// Single permission
access.allow("ADMIN", "USER:DELETE");
// Multiple permissions
access.allow("EDITOR", ["BLOGS:CREATE", "BLOGS:READ", "BLOGS:UPDATE"]);
// With custom ABAC conditions
access.allow("USER", "BLOG:UPDATE", (context) => {
return context.post.authorId === context.user.id;
});2. Dynamic Assignment (Database + Cache)
Best for production apps where permissions are managed in a DB or Admin Panel. This is optional but provides powerful caching and auto-sync benefits.
await access.assignPermissions("ADMIN",
// 1. Fetcher: Returns the list of valid permissions from your DB
async () => {
const permissions = await db.query("SELECT key FROM permissions WHERE role = 'ADMIN'");
return permissions.map(p => p.key);
},
{
// OPTIONAL: A fast key-check (e.g., Redis version or DB timestamp)
// The engine only re-runs the Fetcher if this key changes.
invalidateKey: async () => await redis.get("perms:admin:version"),
// OPTIONAL: Auto-check for updates every 60 seconds in the background
interval: 60000
}
);[!TIP] Extra Benefits: By using
invalidateKey, you avoid hitting your database for every permission check. The engine keeps permissions in an in-memory cache and only refetches when your "version" key in Redis/DB changes.
🔄 Manual Refresh & Sync
If you don't use the interval option, or if you need to force a sync after an admin update, use the refresh method.
// Forces the engine to check invalidateKeys and re-fetch if they changed
await access.refresh();
// Refresh only a specific role
await access.refresh("ADMIN");🎨 Frontend Usage (React)
Frontend components are reactive. When you call access.refresh() or when an interval triggers an update, all components using the hooks will automatically re-render.
1. useCan Hook (Engine Bound)
Automatically re-renders when the engine's permissions for the given role change.
const canEdit = access.useCan("EDITOR", "BLOGS:UPDATE");2. usePermissions Hook (Flexible Source)
Manage permissions from any source (Static, Async, or Role). Provides loading state and a manual refresh trigger.
const { permissions, loading, refresh } = access.usePermissions(async () => {
const res = await api.get("/my-permissions");
return res.data;
});
if (loading) return <Spinner />;
return (
<div>
<button onClick={() => refresh()}>Sync Permissions</button>
<Can permissions={permissions} permission="BLOG:CREATE">
<CreatePost />
</Can>
</div>
);3. <Can /> Component
Works with both roles (engine-bound) and explicit permission arrays.
// Role-based (Reactive)
<access.Can role="ADMIN" permission="USER:DELETE">
<DeleteButton />
</access.Can>
// Permission-based
<access.Can permissions={userPerms} permission="BLOGS:READ">
<PostList />
</access.Can>🧠 Type Safety & Autocomplete
Invalid permission → ❌ TypeScript error
Invalid resource/action → ❌ TypeScript error
IDE auto-suggests valid permissions everywhere
// ❌ Invalid "BLOGS:PUBLISH"
// ✅ Valid "BLOGS:CREATE"
🏗️ API Reference Metadata access.roles access.actions access.resources access.permissions access.permissionKeys
Backend access.allow(role, permission) access.assignPermissions(role, perms, options) // Async: Supports fetchers & invalidation access.can(role, permission) access.resolvePermissions(role) access.normalizePermissions(raw) access.refresh(role?) // Async: Trigger key check and conditional refetch
Frontend access.useCan(permissions, permission) <access.Can />
🔐 Multi-Permission Assignment Assign multiple permissions to a role at once:
access.allow("EDITOR", ["BLOGS:CREATE", "BLOGS:READ", "BLOGS:UPDATE"]);🏆 Why Use @accessx/core?
Zero manual permission creation
DB, backend & frontend always in sync
Enterprise-grade RBAC foundation for apps
Scales to ABAC, multi-role, multi-tenant systems
🧠 ABAC (Attribute-Based Access Control)
You can define dynamic permissions based on context (e.g., user ID, ownership checking).
1. Define with Conditions
access.allow("USER", "BLOG:UPDATE", (context: { user: any; post: any }) => {
return context.post.authorId === context.user.id;
});2. Check with Context
The can method accepts an optional context object as the third argument.
const canEdit = access.can("USER", "BLOG:UPDATE", { user, post });3. Frontend Usage
React components also support context.
<Can permissions={myPermissions} permission="BLOG:UPDATE" context={{ user, post }}>
<button>Edit Post</button>
</Can>🧪 Testing
The package includes a comprehensive test suite using Jest.
npm test🛣️ Roadmap
- [ ] Multi-role users
- [ ] Permission groups
- [ ] JWT permission compression
- [ ] CLI generator
📄 License
MIT
💡 Inspiration
Zanzibar (Google)
Auth0 / Keycloak permission models
CASL & OPA (simplified DX)
One definition. One truth. Everywhere. 🔐✨
