bknd-production-config
npx skills add https://github.com/cameronapak/bknd-skills --skill bknd-production-config
Agent 安装分布
Skill 文档
Configure for Production
Prepare and secure your Bknd application for production deployment.
Prerequisites
- Working Bknd application tested locally
- Database provisioned (see
bknd-database-provision) - Hosting platform selected (see
bknd-deploy-hosting)
When to Use UI Mode
- Viewing current configuration in admin panel
- Verifying Guard settings are active
- Checking auth configuration
When to Use Code Mode
- All production configuration changes
- Setting environment variables
- Configuring security settings
- Setting up adapters
Code Approach
Step 1: Enable Production Mode
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:
- Disables schema auto-sync (prevents accidental migrations)
- Hides detailed error messages from API responses
- Disables admin panel modifications (read-only)
- Enables stricter security defaults
Step 2: Configure JWT Authentication
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
Step 3: Enable Guard (Authorization)
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.
Step 4: Configure CORS
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"],
},
},
},
}),
};
Step 5: Configure Media Storage
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,
},
},
},
}
Complete Production Configuration
// 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;
Environment Variables Template
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=...
Security Checklist
Authentication
- JWT secret is 32+ characters, randomly generated
- JWT secret stored in environment variable, not code
- Cookie
httpOnly: trueset - Cookie
secure: truein production (HTTPS) - Cookie
sameSite: "strict"or"lax" - Password hashing uses bcrypt with rounds >= 10
- Minimum password length enforced (8+ chars)
Authorization
- Guard enabled (
guard.enabled: true) - Default role defined for anonymous users
- Admin role does NOT use
implicit_allowunless intended - Sensitive entities have explicit permissions
- Row-level security filters user-owned data
Data
-
isProduction: trueset - Database credentials in environment variables
- No test/seed data in production
- Backups configured for database
Media
- Cloud storage configured (not local filesystem)
- Storage credentials in environment variables
- CORS configured on storage bucket
- Max upload size limited (
body_max_size)
Network
- CORS origins explicitly listed (no wildcard
*) - HTTPS enforced (via platform/proxy)
- API rate limiting configured (if needed)
Platform-Specific Security
Cloudflare Workers
// 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",
},
},
}),
});
Vercel
# 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
# 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
Testing Production Config Locally
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:
- Admin panel is read-only (no schema changes)
- API errors don’t expose stack traces
- Auth requires valid JWT
- Guard enforces permissions
Common Pitfalls
“JWT_SECRET required” Error
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" ...
Guard Not Enforcing Permissions
Problem: Users can access everything
Fix: Ensure Guard is enabled:
config: {
guard: {
enabled: true, // Must be true!
},
}
Cookies Not Set (CORS Issues)
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,
},
},
}
Admin Panel Allows Changes
Problem: Schema can be modified in production
Fix: Set isProduction: true:
isProduction: true, // Locks admin to read-only
Detailed Errors Exposed
Problem: API returns stack traces
Fix: isProduction: true hides internal errors. Also check for custom error handlers exposing details.
DOs and DON’Ts
DO:
- Set
isProduction: truein production - Generate cryptographically secure JWT secrets (32+ chars)
- Enable Guard for authorization
- Use cloud storage for media
- Set explicit CORS origins
- Use environment variables for all secrets
- Test production config locally first
- Enable HTTPS (via platform/proxy)
- Set cookie
secure: trueandhttpOnly: true
DON’T:
- Use default or weak JWT secrets
- Commit secrets to version control
- Use wildcard (
*) CORS origins - Leave Guard disabled in production
- Use local filesystem storage in serverless
- Expose detailed error messages
- Skip the security checklist
- Use
sha256password hashing (usebcrypt) - Set
implicit_allow: trueon non-admin roles
Related Skills
- bknd-deploy-hosting – Deploy to hosting platforms
- bknd-database-provision – Set up production database
- bknd-env-config – Environment variable setup
- bknd-setup-auth – Authentication configuration
- bknd-create-role – Define authorization roles
- bknd-storage-config – Media storage setup