granter
v2.0.2
Published
Composable, type-safe authorization for TypeScript. Define permissions once, use everywhere.
Maintainers
Readme
granter
Composable, type-safe authorization for TypeScript
📚 Read the full documentation →
Why granter?
✨ Composable - Build complex permissions from simple rules
🔒 Type-safe - Full TypeScript inference with generic contexts
⚡ Async-first - Works seamlessly with databases, APIs, and DataLoader
🔧 Framework-agnostic - Works with Express, Hono, Next.js, GraphQL, and more
🪶 Zero dependencies - Lightweight and performant
Quick Example
import { permission, or } from 'granter';
// Define permissions
const isAdmin = permission('isAdmin', (ctx) => ctx.user.role === 'admin');
const isPostOwner = permission('isPostOwner', (ctx, post) => post.authorId === ctx.user.id);
// Compose permissions
const canEditPost = or(isPostOwner, isAdmin);
// Use them - permissions are callable!
if (await canEditPost(ctx, post)) {
await updatePost(post);
}
// Require permission (throws if denied)
await canEditPost.orThrow(ctx, post);
// Filter arrays
const editablePosts = await canEditPost.filter(ctx, allPosts);
// Debug permission checks
const explanation = await canEditPost.explain(ctx, post);Installation
npm install granterDocumentation
Visit seeden.github.io/granter for the complete documentation:
- Getting Started - Install and use granter in 5 minutes
- Core Concepts - Learn about permissions, operators, and methods
- Express Example - Complete REST API example
- API Reference - Full API documentation
Key Features
Composable Operators
import { and, or, not } from 'granter';
// Combine with OR (any must pass)
const canEdit = or(isPostOwner, isAdmin, isModerator);
// Combine with AND (all must pass)
const canPublish = and(isAuthenticated, isVerified, isPostOwner);
// Negate permissions
const canComment = and(isAuthenticated, not(isBanned));Powerful Methods
// Check permission (returns boolean)
if (await canEdit(ctx, post)) {
/* ... */
}
// Require permission (throws if denied)
await canEdit.orThrow(ctx, post);
// Filter arrays to allowed items
const editable = await canEdit.filter(ctx, allPosts);
// Debug permission checks
const explanation = await canEdit.explain(ctx, post);Simplify with withContext()
import { withContext } from 'granter';
const abilities = withContext(ctx, {
canEditPost,
canDeletePost,
});
// No need to pass ctx anymore!
if (await abilities.canEditPost(post)) {
await updatePost(post);
}Framework Examples
granter works with any TypeScript project. See the documentation for complete examples with:
- Express.js - REST API with middleware
- Next.js - Server Actions and App Router
- GraphQL - Apollo Server with DataLoader
- React - Context and hooks patterns
Authentication Integration
granter is authorization-only and works with any authentication library:
- Auth.js / NextAuth.js
- Clerk
- Supabase Auth
- Custom JWT/Sessions
- And more...
See the Authentication Integration guide for complete examples.
TypeScript Support
granter is built with TypeScript and provides full type inference:
type AppContext = {
user: { id: string; role: string };
db: Database;
};
type Post = {
id: string;
authorId: string;
};
const canEdit = or(isPostOwner, isAdmin);
// ✅ Type-safe: ctx and post are fully typed
await canEdit(ctx, post);
// ❌ TypeScript error: missing resource
await canEdit(ctx);Testing
Permissions are pure functions, making them easy to test:
import { describe, it, expect } from 'vitest';
describe('canEditPost', () => {
it('allows post owner', async () => {
const ctx = { user: { id: '1', role: 'user' }, db };
const post = { id: '123', authorId: '1' };
expect(await canEditPost(ctx, post)).toBe(true);
});
it('allows admin', async () => {
const ctx = { user: { id: '2', role: 'admin' }, db };
const post = { id: '123', authorId: '1' };
expect(await canEditPost(ctx, post)).toBe(true);
});
it('denies other users', async () => {
const ctx = { user: { id: '3', role: 'user' }, db };
const post = { id: '123', authorId: '1' };
expect(await canEditPost(ctx, post)).toBe(false);
});
});Advanced Features
Parallel Operators
Use orParallel() and andParallel() for DataLoader batching:
import { orParallel, andParallel } from 'granter';
// Run all checks in parallel (no short-circuit)
const canEdit = orParallel(isPostOwner, isAdmin, isModerator);Learn more about parallel execution →
Debug with .explain()
Understand why permissions passed or failed:
const explanation = await canEdit.explain(ctx, post);
console.log(JSON.stringify(explanation, null, 2));
// {
// "name": "(isPostOwner OR isAdmin)",
// "value": false,
// "duration": 15.23,
// "children": [
// { "name": "isPostOwner", "value": false, "duration": 8.12 },
// { "name": "isAdmin", "value": false, "duration": 7.11 }
// ]
// }Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT © seeden
📚 View Full Documentation | GitHub | npm
