Custom Permissions
Define permission namespaces and relations for your application
Custom Permissions
OmniBase allows you to define custom permission namespaces and relations that match your application's domain model. Permissions are defined in TypeScript files and synced via the CLI.
UI Components
OmniBase provides pre-built shadcn components for permission management in @omnibase/shadcn:
Project Structure
Custom permissions are defined in your project's omnibase/permissions/ directory:
omnibase/
└── permissions/
├── roles.config.json # Role definitions
├── tenants.ts # Tenant namespace (required)
├── projects.ts # Project namespace (example)
├── documents.ts # Document namespace (example)
└── ... # Your custom namespacesDefining Permission Namespaces
Permission namespaces define the entities in your system and the relations users can have with them.
Basic Namespace Structure
import type { Namespace, Context } from "@ory/keto-namespace-types";
class User implements Namespace {}
class Project implements Namespace {
// Define the relations that can exist
related: {
owners: User[];
editors: User[];
viewers: User[];
};
// Define computed permissions
permits = {
// Only owners can delete
delete: (ctx: Context) =>
this.related.owners.includes(ctx.subject),
// Owners and editors can write
write: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.editors.includes(ctx.subject),
// All relations can read
read: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.editors.includes(ctx.subject) ||
this.related.viewers.includes(ctx.subject),
};
}Tenant Namespace (Required)
The tenant namespace is required and defines tenant-level permissions:
import type { Namespace, Context } from "@ory/keto-namespace-types";
class User implements Namespace {}
class Tenant implements Namespace {
related: {
owners: User[];
admins: User[];
members: User[];
};
permits = {
// Only owners can delete the tenant
delete_tenant: (ctx: Context) =>
this.related.owners.includes(ctx.subject),
// Owners and admins can manage members
invite_user: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject),
remove_user: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject),
update_user_role: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject),
// All members can view the user list
view_users: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject) ||
this.related.members.includes(ctx.subject),
};
}Permission Types
Tenant Permissions (Auto-Scoped)
Tenant permissions are the only permissions that support auto-scoping. The active tenant ID is automatically injected.
When you check a tenant# permission, OmniBase automatically uses the user's active tenant:
// In your roles.config.json
{
"permissions": [
"tenant#delete_tenant", // Auto-scoped to active tenant
"tenant#invite_user" // Auto-scoped to active tenant
]
}Resource Permissions (UUID Required)
Resource permissions do not support wildcards. You must always specify the resource UUID when creating or checking permissions.
For resources like projects, documents, or any custom entities:
// Creating a resource permission - UUID required
await permissionsApi.createRelationship({
createRelationshipRequest: {
namespace: 'Project',
object: 'proj-123-uuid', // Specific UUID required
relation: 'can_write',
subjectId: userId,
subjectNamespace: 'User',
},
});
// Checking a resource permission - UUID required
await permissionsApi.checkPermission({
checkPermissionRequest: {
namespace: 'Project',
object: 'proj-123-uuid', // Specific UUID required
relation: 'can_write',
subjectId: userId,
subjectNamespace: 'User',
},
});Subject Sets
Subject sets allow you to grant permissions to groups of users based on their relationships:
// Instead of granting to each user individually:
(Project, proj-123, viewer, user:alice)
(Project, proj-123, viewer, user:bob)
(Project, proj-123, viewer, user:carol)
// Grant to all members of a tenant:
(Project, proj-123, viewers, Tenant:acme-corp#members)Creating Subject Set Relationships
await permissionsApi.createRelationship({
createRelationshipRequest: {
namespace: 'Project',
object: projectId,
relation: 'viewers',
// Subject set: all members of the tenant
subjectId: tenantId,
subjectNamespace: 'Tenant',
subjectRelation: 'members',
},
});This means "all members of the tenant can view this project".
Hierarchical Permissions
Define permission inheritance across namespaces:
import type { Namespace, Context } from "@ory/keto-namespace-types";
class User implements Namespace {}
class Tenant implements Namespace {
related: {
parent: Organization;
owners: User[];
};
}
class Organization implements Namespace {
related: {
owners: User[];
tenants: Tenant[];
};
permits = {
// Org owners can manage all child tenants
manage_tenants: (ctx: Context) =>
this.related.owners.includes(ctx.subject),
};
}
// In Tenant namespace, inherit from Organization
class TenantWithInheritance implements Namespace {
related: {
parent: Organization;
owners: User[];
};
permits = {
// Tenant owners OR org owners can delete
delete: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.parent.permits.manage_tenants(ctx),
};
}Syncing Permissions
After creating or updating permission files, sync them to OmniBase:
Validate Configuration
omnibase validate permissionsThis checks for syntax errors and invalid references.
Sync to OmniBase
omnibase sync permissionsThis uploads your permission namespaces and role definitions.
Checking Permissions in Code
Direct Permission Check
import { V1PermissionsApi } from '@omnibase/core-js';
const permissionsApi = new V1PermissionsApi(config);
const { data } = await permissionsApi.checkPermission({
checkPermissionRequest: {
namespace: 'Project',
object: projectId,
relation: 'write',
subjectId: userId,
subjectNamespace: 'User',
},
});
if (data.data.allowed) {
// User can write to this project
}Middleware Pattern
async function requirePermission(
namespace: string,
relation: string,
getObject: (req: Request) => string
) {
return async (req: Request, res: Response, next: NextFunction) => {
const { data } = await permissionsApi.checkPermission({
checkPermissionRequest: {
namespace,
object: getObject(req),
relation,
subjectId: req.user.id,
subjectNamespace: 'User',
},
});
if (!data.data.allowed) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// Usage
app.delete('/projects/:id',
requirePermission('Project', 'delete', (req) => req.params.id),
deleteProjectHandler
);Listing Available Permissions
Get all available permission definitions:
const { data } = await tenantsApi.getRoleDefinitions({
subject: 'User', // or 'ApiKey'
});
for (const namespace of data.data.namespaces) {
console.log(`Namespace: ${namespace.name}`);
for (const relation of namespace.relations) {
console.log(` - ${namespace.name}#${relation.name}`);
console.log(` ${relation.description}`);
}
}Subject Types
| Subject | Description |
|---|---|
User | Human users with sessions |
ApiKey | Programmatic API keys |
Some permissions may be restricted to specific subject types.
Example: Document Management System
A complete example with multiple namespaces:
class Tenant implements Namespace {
related: {
owners: User[];
admins: User[];
members: User[];
};
permits = {
delete_tenant: (ctx: Context) =>
this.related.owners.includes(ctx.subject),
manage_members: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject),
view: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.admins.includes(ctx.subject) ||
this.related.members.includes(ctx.subject),
};
}class Folder implements Namespace {
related: {
parent: Folder;
owners: User[];
editors: User[];
viewers: User[];
};
permits = {
delete: (ctx: Context) =>
this.related.owners.includes(ctx.subject),
create_document: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.editors.includes(ctx.subject),
view: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.editors.includes(ctx.subject) ||
this.related.viewers.includes(ctx.subject) ||
this.related.parent?.permits.view(ctx),
};
}class Document implements Namespace {
related: {
parent: Folder;
owners: User[];
editors: User[];
viewers: User[];
};
permits = {
delete: (ctx: Context) =>
this.related.owners.includes(ctx.subject),
edit: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.editors.includes(ctx.subject),
view: (ctx: Context) =>
this.related.owners.includes(ctx.subject) ||
this.related.editors.includes(ctx.subject) ||
this.related.viewers.includes(ctx.subject) ||
this.related.parent?.permits.view(ctx),
};
}Next Steps
- Role Configuration — Configure roles that use these permissions
- Permissions Concept — Deep dive into ReBAC internals
- Data Isolation — How permissions work with RLS