bknd-production-config

📁 cameronapak/bknd-skills 📅 Jan 21, 2026
0
总安装量
6
周安装量
安装命令
npx skills add https://github.com/cameronapak/bknd-skills --skill bknd-production-config

Agent 安装分布

gemini-cli 4
codex 4
cursor 3
antigravity 3
junie 3
windsurf 3

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: 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

// 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:

  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:

# 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: 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