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