home / skills / cameronapak / bknd-skills / bknd-create-role

bknd-create-role skill

/skills/bknd-create-role

This skill helps you define and manage Bknd authorization roles, including permissions, defaults, and hierarchies, for secure, scalable access control.

npx playbooks add skill cameronapak/bknd-skills --skill bknd-create-role

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
9.5 KB
---
name: bknd-create-role
description: Use when defining a new role in Bknd authorization system. Covers role properties (implicit_allow, is_default, permissions), permission assignment, role hierarchies, and common role patterns (admin, editor, viewer, anonymous).
---

# Create Role

Define a new role in Bknd's authorization system to control user access.

## Prerequisites

- Bknd project initialized with code-first configuration
- Auth enabled (`auth: { enabled: true }`)
- Guard enabled for authorization (`guard: { enabled: true }`)

## When to Use UI Mode

- Viewing existing roles
- Quick toggle of role settings

**UI steps:** Admin Panel > Auth > Roles

**Note:** Role creation requires code mode. UI only shows existing roles.

## When to Use Code Mode

- Creating new roles
- Setting role permissions
- Configuring default roles
- Setting up role hierarchies

## Code Approach

### Step 1: Enable Guard

Roles require the guard system to be enabled:

```typescript
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 },  // Required for roles
      roles: {
        // Roles defined here
      },
    },
  },
});
```

### Step 2: Define a Basic Role

Create a role with explicit permissions:

```typescript
{
  auth: {
    enabled: true,
    guard: { enabled: true },
    roles: {
      viewer: {
        implicit_allow: false,  // Deny by default
        permissions: [
          "data.entity.read",   // Grant read access only
        ],
      },
    },
  },
}
```

### Role Properties

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `implicit_allow` | boolean | `false` | Allow all unless denied |
| `is_default` | boolean | `false` | Use when user has no role |
| `permissions` | array | `[]` | Permissions granted to role |

### Step 3: Create Admin Role (Full Access)

Grant full access with `implicit_allow`:

```typescript
{
  roles: {
    admin: {
      implicit_allow: true,  // Can do everything
    },
  },
}
```

**Warning:** `implicit_allow: true` grants ALL permissions. Use only for admin roles.

### Step 4: Create Editor Role (Partial Access)

Grant specific CRUD permissions:

```typescript
{
  roles: {
    editor: {
      implicit_allow: false,
      permissions: [
        "data.entity.read",
        "data.entity.create",
        "data.entity.update",
        // No delete permission
      ],
    },
  },
}
```

### Step 5: Create Default Role

Set a role for users without assigned role:

```typescript
{
  roles: {
    anonymous: {
      is_default: true,       // Applied when no role
      implicit_allow: false,
      permissions: [
        "data.entity.read",   // Read-only access
      ],
    },
  },
}
```

**Note:** Only ONE role can have `is_default: true`.

### Step 6: Set Registration Role

Assign role to newly registered users:

```typescript
{
  auth: {
    enabled: true,
    default_role_register: "user",  // Role for new registrations
    roles: {
      user: {
        implicit_allow: false,
        permissions: ["data.entity.read"],
      },
    },
  },
}
```

## Available Permissions

| Permission | Description |
|------------|-------------|
| `data.entity.read` | Read any entity records |
| `data.entity.create` | Create records in any entity |
| `data.entity.update` | Update records in any entity |
| `data.entity.delete` | Delete records from any entity |
| `data.database.sync` | Sync database schema |
| `data.raw.query` | Execute raw SELECT queries |
| `data.raw.mutate` | Execute raw INSERT/UPDATE/DELETE |

## Common Role Patterns

### Multi-Tier Access System

```typescript
{
  auth: {
    enabled: true,
    guard: { enabled: true },
    default_role_register: "user",
    roles: {
      // Full access
      admin: {
        implicit_allow: true,
      },

      // Content management
      editor: {
        implicit_allow: false,
        permissions: [
          "data.entity.read",
          "data.entity.create",
          "data.entity.update",
          "data.entity.delete",
        ],
      },

      // Create and read
      contributor: {
        implicit_allow: false,
        permissions: [
          "data.entity.read",
          "data.entity.create",
        ],
      },

      // Authenticated read-only
      user: {
        implicit_allow: false,
        permissions: [
          "data.entity.read",
        ],
      },

      // Unauthenticated/guest
      anonymous: {
        is_default: true,
        implicit_allow: false,
        permissions: [
          "data.entity.read",
        ],
      },
    },
  },
}
```

### Closed System (No Public Access)

```typescript
{
  auth: {
    enabled: true,
    guard: { enabled: true },
    allow_register: false,  // Disable self-registration
    roles: {
      admin: {
        implicit_allow: true,
      },
      member: {
        implicit_allow: false,
        permissions: [
          "data.entity.read",
          "data.entity.create",
          "data.entity.update",
        ],
      },
      // No default role - unauthenticated users get NO access
    },
  },
}
```

### API Consumer Role

```typescript
{
  roles: {
    api_client: {
      implicit_allow: false,
      permissions: [
        "data.entity.read",
        "data.entity.create",
        // No update/delete - API clients create data only
      ],
    },
  },
}
```

## Permission Effects

Use extended format for allow/deny effects:

```typescript
{
  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
      ],
    },
  },
}
```

## Role Assignment

### Assign During User Creation (Seed)

```typescript
{
  options: {
    seed: async (ctx) => {
      await ctx.app.module.auth.createUser({
        email: "[email protected]",
        password: "secure-password",
        role: "admin",  // Assign admin role
      });
    },
  },
}
```

### Assign During Registration

```typescript
{
  auth: {
    default_role_register: "user",  // All registrations get "user" role
  },
}
```

### Update User Role (API)

```typescript
const api = getApi(app);

// Update user's role
await api.data.updateOne("users", userId, {
  role: "editor",
});
```

## Verification

Test role permissions:

**1. Create user with role:**

```bash
curl -X POST http://localhost:7654/api/auth/password/register \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "password123"}'
```

**2. Login and get token:**

```bash
curl -X POST http://localhost:7654/api/auth/password/login \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "password123"}'
```

**3. Test permission (should succeed for read):**

```bash
curl http://localhost:7654/api/data/posts \
  -H "Authorization: Bearer <token>"
```

**4. Test denied permission (should fail for delete if not allowed):**

```bash
curl -X DELETE http://localhost:7654/api/data/posts/1 \
  -H "Authorization: Bearer <token>"
# Returns 403 if delete not in permissions
```

## Common Pitfalls

### No Default Role

**Problem:** `User has no role` error for unauthenticated users

**Fix:** Set a default role:

```typescript
{
  roles: {
    anonymous: {
      is_default: true,
      permissions: ["data.entity.read"],
    },
  },
}
```

### Multiple Default Roles

**Problem:** Unpredictable behavior with multiple `is_default: true`

**Fix:** Only ONE role should be default:

```typescript
{
  roles: {
    user: { is_default: true },    // Only one!
    guest: { /* no is_default */ },
  },
}
```

### Role Not Found

**Problem:** `Role "admin" not found` when assigning

**Fix:** Define role before referencing:

```typescript
{
  auth: {
    roles: {
      admin: { implicit_allow: true },  // Define first
    },
    default_role_register: "admin",     // Then reference
  },
}
```

### Guard Not Enabled

**Problem:** Roles defined but permissions not enforced

**Fix:** Enable the guard:

```typescript
{
  auth: {
    enabled: true,
    guard: { enabled: true },  // Required!
    roles: { /* ... */ },
  },
}
```

### Implicit Allow Overuse

**Problem:** Using `implicit_allow: true` on non-admin roles

**Fix:** Be explicit about permissions:

```typescript
// WRONG - too permissive
{
  roles: {
    editor: { implicit_allow: true },
  },
}

// CORRECT - explicit permissions
{
  roles: {
    editor: {
      implicit_allow: false,
      permissions: [
        "data.entity.read",
        "data.entity.create",
        "data.entity.update",
      ],
    },
  },
}
```

## DOs and DON'Ts

**DO:**
- Enable guard when using roles
- Use `implicit_allow: false` for non-admin roles
- Set one default role for unauthenticated access
- Define roles before referencing them
- Test each role's permissions after creation

**DON'T:**
- Use `implicit_allow: true` for non-admin roles
- Set multiple roles as default
- Forget to enable guard
- Grant `data.raw.*` permissions to untrusted roles
- Assume roles work without guard enabled

## Related Skills

- **bknd-setup-auth** - Initialize authentication system
- **bknd-assign-permissions** - Configure detailed permissions with policies
- **bknd-row-level-security** - Implement row-level access control
- **bknd-protect-endpoint** - Secure specific endpoints
- **bknd-public-vs-auth** - Configure public vs authenticated access

Overview

This skill helps you define and manage roles in the Bknd authorization system. It covers role properties (implicit_allow, is_default, permissions), permission assignment formats (allow/deny), role hierarchies, and common role patterns like admin, editor, viewer, and anonymous. Use it to design predictable access models and seed roles for registration or administration.

How this skill works

Roles are declared in code mode inside the auth.guard configuration. Each role can set implicit_allow (global allow unless denied), is_default (applied to unauthenticated users), and an array of permissions or permission objects with effect: allow/deny. The guard must be enabled for roles to be enforced. You assign roles at registration, user creation seeds, or by updating the user record.

When to use it

  • Creating new roles or editing permissions (code mode required)
  • Setting the default role for unauthenticated users
  • Seeding admin or initial users with specific roles
  • Modeling multi-tier access (admin, editor, contributor, anonymous)
  • Restricting public access in closed systems

Best practices

  • Always enable guard: auth.guard.enabled must be true for roles to work
  • Use implicit_allow: false for all non-admin roles to avoid accidental grants
  • Set only one is_default role to avoid unpredictable behavior
  • Define roles before referencing them in default_role_register or seeding
  • Limit raw database permissions (data.raw.*) to fully trusted roles

Example use cases

  • Admin role with implicit_allow: true for full system control
  • Editor role granting create/read/update but not delete
  • Anonymous default role granting read-only access to public resources
  • API client role that can create and read but not modify or delete
  • Closed system where allow_register is false and no default role exists

FAQ

What does implicit_allow do?

implicit_allow: true grants all permissions by default unless an explicit deny is present. Use it only for full-admin roles.

How do I give a user a role at signup?

Set auth.default_role_register to the role name to auto-assign it on registration, or create users in a seed script with the role field.

Why are my role permissions not enforced?

Most likely the guard is not enabled. Ensure auth.enabled and auth.guard.enabled are both true.