Use when initializing or configuring the Bknd authentication system. Covers enabling auth, configuring password strategy, setting up JWT and cookie options, defining roles, and production security settings.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Initialize and configure the Bknd authentication system with strategies, JWT, cookies, and roles.
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`.
Initialize and configure the Bknd authentication system with strategies, JWT, cookies, and roles.
UI steps: Admin Panel > Auth > Settings
Note: Full auth configuration requires code mode. UI only shows/toggles existing settings.
Start with basic password authentication:
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,
},
},
});
This enables:
users entity/api/auth/* endpointsJWT tokens authenticate API requests. Configure for security:
{
auth: {
enabled: true,
jwt: {
secret: process.env.JWT_SECRET, // Required in production
alg: "HS256", // Algorithm: HS256, HS384, HS512
expires: 604800, // Expiry in seconds (7 days)
issuer: "my-app", // Optional issuer claim
fields: ["id", "email", "role"], // Fields included in token
},
},
}
JWT options:
| Option | Type | Default | Description |
|---|---|---|---|
secret | string | "" | Signing secret (256-bit minimum for production) |
alg | string | "HS256" | Algorithm: HS256, HS384, HS512 |
expires | number | - | Token expiry in seconds |
issuer | string | - | Token issuer claim (iss) |
fields | string[] | ["id", "email", "role"] | User fields in payload |
Auth cookies store JWT tokens for browser sessions:
{
auth: {
enabled: true,
jwt: { secret: process.env.JWT_SECRET },
cookie: {
secure: true, // HTTPS only (set false for local dev)
httpOnly: true, // Block JavaScript access
sameSite: "lax", // CSRF protection: "strict" | "lax" | "none"
expires: 604800, // Cookie expiry in seconds (7 days)
path: "/", // Cookie path scope
renew: true, // Auto-extend on requests
pathSuccess: "/", // Redirect after login
pathLoggedOut: "/", // Redirect after logout
},
},
}
Cookie options:
| Option | Type | Default | Description |
|---|---|---|---|
secure | boolean | true | HTTPS-only flag |
httpOnly | boolean | true | Block JS access |
sameSite | string | "lax" | CSRF protection |
expires | number | 604800 | Expiry in seconds |
renew | boolean | true | Auto-extend expiry |
pathSuccess | string | "/" | Post-login redirect |
pathLoggedOut | string | "/" | Post-logout redirect |
Set up password hashing and requirements:
{
auth: {
enabled: true,
jwt: { secret: process.env.JWT_SECRET },
strategies: {
password: {
type: "password",
enabled: true,
config: {
hashing: "bcrypt", // "plain" | "sha256" | "bcrypt"
rounds: 4, // bcrypt rounds (1-10)
minLength: 8, // Minimum password length
},
},
},
},
}
Hashing options:
| Option | Security | Performance | Use Case |
|---|---|---|---|
plain | None | Fastest | Development only, never production |
sha256 | Good | Fast | Default, suitable for most cases |
bcrypt | Best | Slower | Recommended for production |
Configure roles for authorization:
{
auth: {
enabled: true,
jwt: { secret: process.env.JWT_SECRET },
roles: {
admin: {
implicit_allow: true, // Can do everything
},
editor: {
implicit_allow: false,
permissions: [
{ permission: "data.posts.read", effect: "allow" },
{ permission: "data.posts.create", effect: "allow" },
{ permission: "data.posts.update", effect: "allow" },
],
},
user: {
implicit_allow: false,
is_default: true, // Default role for new registrations
permissions: [
{ permission: "data.posts.read", effect: "allow" },
],
},
},
default_role_register: "user", // Role assigned on registration
},
}
Control user self-registration:
{
auth: {
enabled: true,
allow_register: true, // Enable/disable registration
default_role_register: "user", // Role for new users
entity_name: "users", // User entity name (default: "users")
basepath: "/api/auth", // Auth API base path
},
}
Complete auth setup with security best practices:
import { serve, type BunBkndConfig } from "bknd/adapter/bun";
import { em, entity, text, date } from "bknd";
const schema = em({
users: entity("users", {
email: text().required().unique(),
name: text(),
avatar: text(),
created_at: date({ default_value: "now" }),
}),
posts: entity("posts", {
title: text().required(),
content: text(),
}),
});
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
const config: BunBkndConfig = {
connection: { url: process.env.DB_URL || "file:data.db" },
config: {
data: schema.toJSON(),
auth: {
enabled: true,
basepath: "/api/auth",
entity_name: "users",
allow_register: true,
default_role_register: "user",
// JWT configuration
jwt: {
secret: process.env.JWT_SECRET!,
alg: "HS256",
expires: 604800, // 7 days
issuer: "my-app",
fields: ["id", "email", "role"],
},
// Cookie configuration
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
expires: 604800,
renew: true,
pathSuccess: "/dashboard",
pathLoggedOut: "/login",
},
// Password strategy
strategies: {
password: {
type: "password",
enabled: true,
config: {
hashing: "bcrypt",
rounds: 4,
minLength: 8,
},
},
},
// Roles
roles: {
admin: {
implicit_allow: true,
},
editor: {
implicit_allow: false,
permissions: [
{ permission: "data.posts.read", effect: "allow" },
{ permission: "data.posts.create", effect: "allow" },
{ permission: "data.posts.update", effect: "allow" },
{ permission: "data.posts.delete", effect: "allow" },
],
},
user: {
implicit_allow: false,
is_default: true,
permissions: [
{ permission: "data.posts.read", effect: "allow" },
],
},
},
},
},
options: {
seed: async (ctx) => {
// Create initial admin on first run
const adminExists = await ctx.em.repo("users").findOne({
where: { email: { $eq: "admin@example.com" } },
});
if (!adminExists) {
await ctx.app.module.auth.createUser({
email: "admin@example.com",
password: process.env.ADMIN_PASSWORD || "changeme123",
role: "admin",
});
console.log("Admin user created");
}
},
},
};
serve(config);
After setup, these endpoints are available:
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/password/login | Login with email/password |
| POST | /api/auth/password/register | Register new user |
| GET | /api/auth/me | Get current user |
| POST | /api/auth/logout | Log out (clear cookie) |
| GET | /api/auth/strategies | List enabled strategies |
Recommended env vars for auth:
# .env
JWT_SECRET=your-256-bit-secret-minimum-32-characters-long
ADMIN_PASSWORD=secure-initial-admin-password
Generate a secure secret:
# Generate 64-character random string
openssl rand -hex 32
| Setting | Development | Production |
|---|---|---|
jwt.secret | Can use placeholder | Required, strong secret |
cookie.secure | false | true (HTTPS only) |
strategies.password.config.hashing | sha256 | bcrypt |
allow_register | true | Consider false for closed systems |
Dev config shortcut:
const isDev = process.env.NODE_ENV !== "production";
{
auth: {
enabled: true,
jwt: {
secret: isDev ? "dev-secret-not-for-production" : process.env.JWT_SECRET!,
expires: isDev ? 86400 * 30 : 604800, // 30 days dev, 7 days prod
},
cookie: {
secure: !isDev,
},
strategies: {
password: {
type: "password",
config: {
hashing: isDev ? "sha256" : "bcrypt",
},
},
},
},
}
Problem: Cannot sign JWT without secret error
Fix: Set JWT secret via environment variable:
{
auth: {
jwt: {
secret: process.env.JWT_SECRET, // Never hardcode in production
},
},
}
Problem: Auth cookie not set in browser
Fix: Set secure: false for local development:
{
auth: {
cookie: {
secure: process.env.NODE_ENV === "production", // false for localhost
},
},
}
Problem: Role "admin" not found when creating users
Fix: Define roles before referencing them:
{
auth: {
roles: {
admin: { implicit_allow: true }, // Define first
user: { implicit_allow: false },
},
default_role_register: "user", // Now can reference
},
}
Problem: Registration not allowed error
Fix: Enable registration:
{
auth: {
allow_register: true, // Default is true, but check if explicitly disabled
},
}
Problem: Using plain or sha256 in production
Fix: Use bcrypt for production:
{
auth: {
strategies: {
password: {
config: {
hashing: "bcrypt",
rounds: 4, // Balance security and performance
},
},
},
},
}
After setup, verify auth works:
1. Check enabled strategies:
curl http://localhost:7654/api/auth/strategies
2. Register a test user:
curl -X POST http://localhost:7654/api/auth/password/register \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password123"}'
3. Login:
curl -X POST http://localhost:7654/api/auth/password/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password123"}'
4. Check current user (with token):
curl http://localhost:7654/api/auth/me \
-H "Authorization: Bearer <token-from-login>"
Before deploying to production:
jwt.secret (256-bit minimum)hashing: "bcrypt" for password strategycookie.secure: true (HTTPS only)cookie.httpOnly: true (default)cookie.sameSite: "lax" or "strict"jwt.expires (don't leave unlimited)allow_register settingDO:
DON'T:
plain hashing in productioncookie.secure in production