Use when assigning permissions to roles in Bknd. Covers permission syntax (simple strings, extended format), permission effects (allow/deny), policies with conditions, entity-specific permissions, and fine-grained access control patterns.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Configure detailed permissions for roles using simple strings, extended format with effects, and conditional policies.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Configure detailed permissions for roles using simple strings, extended format with effects, and conditional policies.
auth: { enabled: true })guard: { enabled: true })UI steps: Admin Panel > Auth > Roles > Select role
Note: Permission assignment requires code mode. UI is read-only.
Assign basic permissions as string array:
import { serve } from "bknd/adapter/bun";
import { em, entity, text } from "bknd";
const schema = em({
posts: entity("posts", { title: text().required() }),
});
serve({
connection: { url: "file:data.db" },
config: {
data: schema.toJSON(),
auth: {
enabled: true,
guard: { enabled: true },
roles: {
editor: {
implicit_allow: false,
permissions: [
"data.entity.read", // Read any entity
"data.entity.create", // Create in any entity
"data.entity.update", // Update any entity
// No delete permission
],
},
},
},
},
});
| Permission | Filterable | Description |
|---|---|---|
data.entity.read | Yes | Read entity records |
data.entity.create | Yes | Create new records |
data.entity.update | Yes | Update existing records |
data.entity.delete | Yes | Delete records |
data.database.sync | No | Sync database schema |
data.raw.query | No | Execute raw SELECT |
data.raw.mutate | No | Execute raw INSERT/UPDATE/DELETE |
Filterable means you can add conditions/filters via policies.
Use objects for explicit allow/deny effects:
{
roles: {
moderator: {
implicit_allow: false,
permissions: [
{ permission: "data.entity.read", effect: "allow" },
{ permission: "data.entity.update", effect: "allow" },
{ permission: "data.entity.delete", effect: "deny" }, // Explicit deny
],
},
},
}
| Effect | Description |
|---|---|
allow | Grant the permission (default) |
deny | Explicitly block the permission |
Deny overrides allow - useful when implicit_allow: true but you want to block specific actions.
Add policies for fine-grained control:
{
roles: {
content_editor: {
implicit_allow: false,
permissions: [
{
permission: "data.entity.read",
effect: "allow",
policies: [
{
description: "Only read posts and comments",
condition: { entity: { $in: ["posts", "comments"] } },
effect: "allow",
},
],
},
{
permission: "data.entity.create",
effect: "allow",
policies: [
{
condition: { entity: { $in: ["posts", "comments"] } },
effect: "allow",
},
],
},
],
},
},
}
{
description?: string, // Human-readable (optional)
condition?: ObjectQuery, // When policy applies
effect: "allow" | "deny" | "filter",
filter?: ObjectQuery, // Row filter (for effect: "filter")
}
| Effect | Purpose |
|---|---|
allow | Grant when condition met |
deny | Block when condition met |
filter | Apply row-level filter to results |
| Operator | Description | Example |
|---|---|---|
$eq | Equal | { entity: { $eq: "posts" } } |
$ne | Not equal | { entity: { $ne: "users" } } |
$in | In array | { entity: { $in: ["posts", "comments"] } } |
$nin | Not in array | { entity: { $nin: ["users", "secrets"] } } |
$gt | Greater than | { age: { $gt: 18 } } |
$gte | Greater or equal | { level: { $gte: 5 } } |
$lt | Less than | { count: { $lt: 100 } } |
$lte | Less or equal | { priority: { $lte: 3 } } |
Reference runtime context with @variable:
| Placeholder | Description |
|---|---|
@user.id | Current user's ID |
@user.email | Current user's email |
@user.role | Current user's role |
@entity | Current entity name |
@id | Current record ID |
Example - user can only update their own profile:
{
permissions: [
{
permission: "data.entity.update",
effect: "allow",
policies: [
{
condition: { entity: "users", "@id": "@user.id" },
effect: "allow",
},
],
},
],
}
Grant different permissions per entity:
{
roles: {
blog_author: {
implicit_allow: false,
permissions: [
// Full CRUD on posts
{
permission: "data.entity.read",
effect: "allow",
policies: [{ condition: { entity: "posts" }, effect: "allow" }],
},
{
permission: "data.entity.create",
effect: "allow",
policies: [{ condition: { entity: "posts" }, effect: "allow" }],
},
{
permission: "data.entity.update",
effect: "allow",
policies: [{ condition: { entity: "posts" }, effect: "allow" }],
},
{
permission: "data.entity.delete",
effect: "allow",
policies: [{ condition: { entity: "posts" }, effect: "allow" }],
},
// Read-only on categories
{
permission: "data.entity.read",
effect: "allow",
policies: [{ condition: { entity: "categories" }, effect: "allow" }],
},
],
},
},
}
{
roles: {
viewer: {
implicit_allow: false,
permissions: ["data.entity.read"],
},
},
}
{
roles: {
contributor: {
implicit_allow: false,
permissions: [
"data.entity.read",
"data.entity.create",
"data.entity.update",
{ permission: "data.entity.delete", effect: "deny" },
],
},
},
}
{
roles: {
admin: {
implicit_allow: true, // Allow all by default
permissions: [
// But deny raw database access
{ permission: "data.raw.query", effect: "deny" },
{ permission: "data.raw.mutate", effect: "deny" },
],
},
},
}
{
roles: {
content_manager: {
implicit_allow: false,
permissions: [
// Content entities: full CRUD
{
permission: "data.entity.read",
effect: "allow",
policies: [{
condition: { entity: { $in: ["posts", "pages", "comments", "media"] } },
effect: "allow",
}],
},
{
permission: "data.entity.create",
effect: "allow",
policies: [{
condition: { entity: { $in: ["posts", "pages", "comments", "media"] } },
effect: "allow",
}],
},
{
permission: "data.entity.update",
effect: "allow",
policies: [{
condition: { entity: { $in: ["posts", "pages", "comments", "media"] } },
effect: "allow",
}],
},
{
permission: "data.entity.delete",
effect: "allow",
policies: [{
condition: { entity: { $in: ["posts", "pages", "comments"] } }, // No media delete
effect: "allow",
}],
},
],
},
},
}
{
roles: {
user: {
implicit_allow: false,
permissions: [
// Can read most entities
"data.entity.read",
// But never access secrets entity
{
permission: "data.entity.read",
effect: "deny",
policies: [{
condition: { entity: "secrets" },
effect: "deny",
}],
},
],
},
},
}
For complex role definitions:
// helpers/permissions.ts
type EntityPermission = "read" | "create" | "update" | "delete";
function entityPermissions(
entities: string[],
actions: EntityPermission[]
) {
const permMap: Record<EntityPermission, string> = {
read: "data.entity.read",
create: "data.entity.create",
update: "data.entity.update",
delete: "data.entity.delete",
};
return actions.map((action) => ({
permission: permMap[action],
effect: "allow" as const,
policies: [{
condition: { entity: { $in: entities } },
effect: "allow" as const,
}],
}));
}
// Usage
{
roles: {
blog_author: {
implicit_allow: false,
permissions: [
...entityPermissions(["posts", "comments"], ["read", "create", "update"]),
...entityPermissions(["categories", "tags"], ["read"]),
],
},
},
}
Test permission assignments:
1. Login as user with role:
curl -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "editor@example.com", "password": "password123"}'
2. Test allowed permission:
curl http://localhost:7654/api/data/posts \
-H "Authorization: Bearer <token>"
# Should return 200 with data
3. Test denied permission:
curl -X DELETE http://localhost:7654/api/data/posts/1 \
-H "Authorization: Bearer <token>"
# Should return 403 Forbidden
4. Test entity-specific permission:
# If only posts/comments allowed:
curl http://localhost:7654/api/data/users \
-H "Authorization: Bearer <token>"
# Should return 403 if users entity not in allowed list
Problem: Changed permissions but old behavior persists
Fix: Restart server - role config is loaded at startup:
# Stop and restart
bknd run
Problem: Deny effect not blocking access
Fix: Check policy condition - deny only applies when condition matches:
// WRONG - no condition, may not match
{ permission: "data.entity.delete", effect: "deny" }
// CORRECT - simple deny at permission level
{
permissions: [
"data.entity.read",
"data.entity.create",
// Don't include delete at all
],
}
Problem: Entity-specific permission not working
Fix: Verify entity name matches exactly:
// WRONG - entity name case matters
{ condition: { entity: "Posts" } }
// CORRECT - use exact entity name
{ condition: { entity: "posts" } }
Problem: Confusing behavior with multiple policies
Fix: Understand evaluation order - first matching policy wins:
{
policies: [
// More specific first
{ condition: { entity: "secrets" }, effect: "deny" },
// General fallback last
{ effect: "allow" },
],
}
Problem: @user.id appearing literally in filter
Fix: Variables only work in filter and condition fields:
// CORRECT usage
{
condition: { "@id": "@user.id" }, // Works
filter: { user_id: "@user.id" }, // Works
}
DO:
$in operator for multiple entitiesimplicit_allowDON'T:
data.raw.* to non-admin roles (SQL injection risk)implicit_allow: true with deny policies (confusing)