home / skills / cameronapak / bknd-skills / bknd-production-config
This skill helps you prepare a BKND app for production by configuring production mode, JWT security, guards, CORS, media storage, and deployment readiness.
npx playbooks add skill cameronapak/bknd-skills --skill bknd-production-configReview the files below or copy the command above to add this skill to your agents.
---
name: bknd-production-config
description: 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.
---
# 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:
```typescript
// 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.
```typescript
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:**
```bash
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# OpenSSL
openssl rand -hex 32
```
### Step 3: Enable Guard (Authorization)
```typescript
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
```typescript
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:
```typescript
// 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
```typescript
// 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:
```bash
# 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: true` set
- [ ] Cookie `secure: true` in 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_allow` unless intended
- [ ] Sensitive entities have explicit permissions
- [ ] Row-level security filters user-owned data
### Data
- [ ] `isProduction: true` set
- [ ] 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
```typescript
// 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
```bash
# 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
```yaml
# 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:
```bash
# 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:**
1. Admin panel is read-only (no schema changes)
2. API errors don't expose stack traces
3. Auth requires valid JWT
4. Guard enforces permissions
---
## Common Pitfalls
### "JWT_SECRET required" Error
**Problem:** Auth fails at startup
**Fix:** Ensure JWT_SECRET is set and accessible:
```bash
# 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:
```typescript
config: {
guard: {
enabled: true, // Must be true!
},
}
```
### Cookies Not Set (CORS Issues)
**Problem:** Auth works in Postman but not browser
**Fix:**
```typescript
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`:
```typescript
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: true` in 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: true` and `httpOnly: 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 `sha256` password hashing (use `bcrypt`)
- Set `implicit_allow: true` on 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
This skill helps prepare a Bknd application for secure, reliable production deployment. It guides enabling production mode, locking down authentication and authorization, configuring CORS and media storage, and validating environment variables. The content focuses on concrete configuration snippets, checklist items, and common fixes so you can deploy with confidence.
The skill inspects your intended production settings and explains the code and environment changes required: set isProduction to true, provide a strong JWT secret, enable Guard, and configure storage adapters. It walks through auth cookie settings, CORS origins, media adapters (S3, R2, Cloudinary), and a security checklist to verify before go-live. Sample configuration blocks and platform-specific notes (Vercel, Cloudflare, Docker) make it actionable.
What happens if isProduction is not set?
Development features stay enabled: schema sync, detailed errors, and writable admin—raising security and stability risks.
How do I generate a secure JWT secret?
Use crypto random tools: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" or openssl rand -hex 32, then store the value in an environment variable.
Can I use wildcard CORS in production?
No. Avoid '*' in production. Explicitly list allowed origins and enable credentials only for trusted frontends.
Why enable Guard?
Without Guard enabled, authenticated users may have excessive access. Guard enforces role permissions and row-level filters to protect sensitive data.