Use when preparing a Bknd application for production deployment. Covers security hardening, environment configuration, isProduction flag, JWT settings, Guard enablement, CORS, media storage, and production checklist.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Prepare and secure your Bknd application for production deployment.
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`.
Prepare and secure your Bknd application for production deployment.
bknd-database-provision)bknd-deploy-hosting)Set isProduction: true to disable development features:
// bknd.config.ts
export default {
app: (env) => ({
connection: { url: env.DB_URL },
isProduction: true, // or env.NODE_ENV === "production"
}),
};
What isProduction: true does:
Critical: Never use default or weak JWT secrets in production.
export default {
app: (env) => ({
connection: { url: env.DB_URL },
isProduction: true,
auth: {
jwt: {
secret: env.JWT_SECRET, // Required, min 32 chars
alg: "HS256", // Or "HS384", "HS512"
expires: "7d", // Token lifetime
issuer: "my-app", // Optional, identifies token source
fields: ["id", "email", "role"], // Claims in token
},
cookie: {
httpOnly: true, // Prevent XSS access
secure: true, // HTTPS only
sameSite: "strict", // CSRF protection
expires: 604800, // 7 days in seconds
},
},
}),
};
Generate secure secret:
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# OpenSSL
openssl rand -hex 32
export default {
app: (env) => ({
connection: { url: env.DB_URL },
isProduction: true,
config: {
guard: {
enabled: true, // Enforce all permissions
},
},
}),
};
Without Guard enabled, all authenticated users have full access.
export default {
app: (env) => ({
// ...
config: {
server: {
cors: {
origin: env.ALLOWED_ORIGINS?.split(",") ?? ["https://myapp.com"],
credentials: true, // Allow cookies
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
},
},
},
}),
};
Never use local storage in production serverless. Use cloud providers:
// AWS S3
export default {
app: (env) => ({
// ...
config: {
media: {
enabled: true,
body_max_size: 10 * 1024 * 1024, // 10MB max upload
adapter: {
type: "s3",
config: {
bucket: env.S3_BUCKET,
region: env.S3_REGION,
accessKeyId: env.S3_ACCESS_KEY,
secretAccessKey: env.S3_SECRET_KEY,
},
},
},
},
}),
};
// Cloudflare R2
config: {
media: {
adapter: {
type: "r2",
config: { bucket: env.R2_BUCKET },
},
},
}
// Cloudinary
config: {
media: {
adapter: {
type: "cloudinary",
config: {
cloudName: env.CLOUDINARY_CLOUD,
apiKey: env.CLOUDINARY_KEY,
apiSecret: env.CLOUDINARY_SECRET,
},
},
},
}
// bknd.config.ts
import type { CliBkndConfig } from "bknd";
import { em, entity, text, relation, enumm } from "bknd";
const schema = em(
{
users: entity("users", {
email: text().required().unique(),
name: text(),
role: enumm(["admin", "user"]).default("user"),
}),
posts: entity("posts", {
title: text().required(),
content: text(),
published: enumm(["draft", "published"]).default("draft"),
}),
},
({ users, posts }) => ({
post_author: relation(posts, users), // posts.author_id -> users
})
);
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
export default {
app: (env) => ({
// Database
connection: {
url: env.DB_URL,
authToken: env.DB_TOKEN,
},
// Schema
schema,
// Production mode
isProduction: env.NODE_ENV === "production",
// Authentication
auth: {
enabled: true,
jwt: {
secret: env.JWT_SECRET,
alg: "HS256",
expires: "7d",
fields: ["id", "email", "role"],
},
cookie: {
httpOnly: true,
secure: env.NODE_ENV === "production",
sameSite: "strict",
expires: 604800,
},
strategies: {
password: {
enabled: true,
hashing: "bcrypt",
rounds: 12,
minLength: 8,
},
},
allow_register: true,
default_role_register: "user",
},
// Authorization
config: {
guard: {
enabled: true,
},
roles: {
admin: {
implicit_allow: true, // Full access
},
user: {
implicit_allow: false,
permissions: [
"data.posts.read",
{
permission: "data.posts.create",
effect: "allow",
},
{
permission: "data.posts.update",
effect: "filter",
condition: { author_id: "@user.id" },
},
{
permission: "data.posts.delete",
effect: "filter",
condition: { author_id: "@user.id" },
},
],
},
anonymous: {
implicit_allow: false,
is_default: true, // Unauthenticated users
permissions: [
{
permission: "data.posts.read",
effect: "filter",
condition: { published: "published" },
},
],
},
},
// Media storage
media: {
enabled: true,
body_max_size: 10 * 1024 * 1024,
adapter: {
type: "s3",
config: {
bucket: env.S3_BUCKET,
region: env.S3_REGION,
accessKeyId: env.S3_ACCESS_KEY,
secretAccessKey: env.S3_SECRET_KEY,
},
},
},
// CORS
server: {
cors: {
origin: env.ALLOWED_ORIGINS?.split(",") ?? [],
credentials: true,
},
},
},
}),
} satisfies CliBkndConfig;
Create .env.production or set in your platform:
# Required
NODE_ENV=production
DB_URL=libsql://your-db.turso.io
DB_TOKEN=your-turso-token
JWT_SECRET=your-64-char-random-secret-here-generate-with-openssl
# CORS
ALLOWED_ORIGINS=https://myapp.com,https://www.myapp.com
# Media Storage (S3)
S3_BUCKET=my-bucket
S3_REGION=us-east-1
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=secret...
# Or Cloudinary
CLOUDINARY_CLOUD=my-cloud
CLOUDINARY_KEY=123456
CLOUDINARY_SECRET=secret
# OAuth (if used)
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
httpOnly: true setsecure: true in production (HTTPS)sameSite: "strict" or "lax"guard.enabled: true)implicit_allow unless intendedisProduction: true setbody_max_size)*)// Secrets set via wrangler
// wrangler secret put JWT_SECRET
// wrangler secret put DB_TOKEN
export default hybrid<CloudflareBkndConfig>({
app: (env) => ({
connection: d1Sqlite({ binding: env.DB }),
isProduction: true,
auth: {
jwt: { secret: env.JWT_SECRET },
cookie: {
httpOnly: true,
secure: true,
sameSite: "strict",
},
},
}),
});
# Set via Vercel CLI or dashboard
vercel env add JWT_SECRET production
vercel env add DB_URL production
vercel env add DB_TOKEN production
# docker-compose.yml
services:
bknd:
environment:
- NODE_ENV=production
- JWT_SECRET=${JWT_SECRET} # From .env or host
# Never put secrets directly in docker-compose.yml
Test with production-like settings before deploying:
# Create .env.production.local (gitignored)
NODE_ENV=production
DB_URL=libsql://test-db.turso.io
DB_TOKEN=test-token
JWT_SECRET=test-secret-min-32-characters-here
# Run with production env
NODE_ENV=production bun run index.ts
# Or source the file
source .env.production.local && bun run index.ts
Verify:
Problem: Auth fails at startup
Fix: Ensure JWT_SECRET is set and accessible:
# Check env is loaded
echo $JWT_SECRET
# Cloudflare: set secret
wrangler secret put JWT_SECRET
# Docker: pass env
docker run -e JWT_SECRET="your-secret" ...
Problem: Users can access everything
Fix: Ensure Guard is enabled:
config: {
guard: {
enabled: true, // Must be true!
},
}
Problem: Auth works in Postman but not browser
Fix:
auth: {
cookie: {
sameSite: "lax", // "strict" may block OAuth redirects
secure: true,
},
},
config: {
server: {
cors: {
origin: ["https://your-frontend.com"], // Explicit, not "*"
credentials: true,
},
},
}
Problem: Schema can be modified in production
Fix: Set isProduction: true:
isProduction: true, // Locks admin to read-only
Problem: API returns stack traces
Fix: isProduction: true hides internal errors. Also check for custom error handlers exposing details.
DO:
isProduction: true in productionsecure: true and httpOnly: trueDON'T:
*) CORS originssha256 password hashing (use bcrypt)implicit_allow: true on non-admin roles