06 — Configuration Reference
Complete reference for all bext configuration files.
Configuration Files
| File | Purpose | Used by |
|---|---|---|
bext.config.toml |
Single-app server config | bext run, bext dev |
platform.toml |
Multi-app platform config | bext serve |
bext.plugin.toml |
Plugin manifest (inside WASM plugin project) | Plugin SDK |
bext.config.toml — Single App Configuration
[server]
[server]
# HTTP listen address
listen = "0.0.0.0:3000" # default: "0.0.0.0:3000"
# App directory (Next.js app dir, Hono src, etc.)
app_dir = "src/app" # default: auto-detected
# Static file directory
static_dir = "./public" # default: auto-detected (public/ or static/)
# Hybrid mode: proxy cache misses to this URL
bun_api_url = "http://localhost:3060" # default: none (standalone mode)
[runtime]
[runtime]
# Framework override (auto-detected if omitted)
# framework = "nextjs" # "nextjs" | "hono" | "fetch" | "express" | "static"
# Transform profile (defaults based on framework)
# transform_profile = "nextjs" # "nextjs" | "hono" | "generic" | "none"
# TypeScript handling
typescript_strip = true # default: true for non-bundled apps
# Module resolution (for non-bundled apps)
# resolve_node_modules = true # default: true
[cache]
[cache.isr]
max_entries = 10_000 # default: 10,000
default_ttl_ms = 60_000 # default: 60s
default_swr_ms = 3_600_000 # default: 1h (stale-while-revalidate)
[cache.tenant]
ttl_ms = 300_000 # default: 5min
[cache.fragment]
max_entries = 5_000 # default: 5,000
max_bytes = 52_428_800 # default: 50MB
[cache.layout]
max_entries = 100 # default: 100
ttl_ms = 300_000 # default: 5min
[render]
[render]
# JSC render workers (0 = disable JSC)
jsc_workers = 4 # default: 4
# SSR bundle path
bundle_path = "dist/ssr-bundle.js" # default: none
# Per-isolate limits (platform mode)
# memory_limit_mb = 128 # default: 128
# execution_timeout_ms = 5000 # default: 5000
[build]
[build]
# Build script (runs on startup and file change)
script = "scripts/build-ssr.ts" # default: none
# Watch directories for auto-rebuild (dev mode)
watch_dirs = ["src/app", "src/components"] # default: none
# Build timeout
timeout_secs = 120 # default: 120
# Bundler (for bext-managed builds)
# bundler = "bun" # "bun" | "esbuild" | "custom"
[auth]
[auth]
# JWT secret (also reads JWT_SECRET env var)
# jwt_secret = "..."
# Skip auth for localhost
bypass_localhost = true # default: true
# Session cookie name
cookie_name = "session" # default: "session"
[cors]
[cors]
# Allowed origins (empty = allow all)
allowed_origins = [] # default: [] (allow all)
# Preflight cache duration
max_age_secs = 3600 # default: 3600
[rate_limit]
[rate_limit]
enabled = true # default: true
requests_per_minute = 600 # default: 600
[i18n]
[i18n]
# Supported locales (empty = disabled)
locales = ["en", "fr", "es"] # default: []
# Default locale
default_locale = "en" # default: "en"
# Routing strategy
strategy = "prefix" # "prefix" | "accept-language"
[database]
[database]
# PostgreSQL URL (also reads DATABASE_URL env var)
# url = "postgresql://..."
# Connection pool
min_connections = 2 # default: 2
max_connections = 10 # default: 10
[middleware]
[middleware]
# User middleware file (Next.js middleware.ts equivalent)
# path = "src/middleware.ts" # default: auto-detected
# Enable/disable
enabled = true # default: auto-detected
[transforms]
[transforms]
# Transform profile (auto-detected from framework)
# profile = "nextjs"
# Override individual transforms
# enabled = ["env_inline", "barrel_optimize", "font_optimize"]
[transforms.env_inline]
prefix = "NEXT_PUBLIC_" # default: "NEXT_PUBLIC_"
[transforms.barrel_optimize]
packages = ["lucide-react", "@heroicons/react"] # default: ["lucide-react"]
[transforms.import_strip]
packages = ["server-only"] # default: ["server-only"]
[redis] (Feature-Gated: --features redis)
Horizontal scaling via Redis. When url is set, the server runs in cluster mode with shared state across instances. When absent, runs in solo mode with zero Redis overhead.
[redis]
# Redis connection URL (also reads REDIS_URL env var)
url = "redis://localhost:6379" # default: none (solo mode)
# Key prefix for all Redis keys
prefix = "bext:" # default: "bext:"
What cluster mode enables:
| Feature | Solo | Cluster |
|---|---|---|
| ISR cache | L1 only (in-memory LRU) | L1 + L2 (Redis write-through) |
| Rate limiting | Per-instance counters | Shared Redis INCR counters |
| Cache invalidation | Local only | Local + L2 + Pub/Sub broadcast |
Redis key structure:
| Pattern | Purpose | TTL |
|---|---|---|
{prefix}isr:{cache_key} |
ISR HTML + metadata (JSON) | ttl_ms + swr_ms |
{prefix}tag:{tag} |
SET of ISR keys with this tag | Same as entry |
{prefix}rl:{ip} |
Rate limit counter | 60s |
{prefix}invalidate |
Pub/Sub channel | — |
Graceful degradation: If Redis becomes unreachable during operation, every component falls back to solo-mode behavior (local caches, local rate limiting). No crashes, no data loss.
L2 flow:
- Request arrives → check L1 (in-memory). Hit? Serve immediately.
- L1 miss → check L2 (Redis). Hit? Populate L1 with remaining TTL, serve.
- L2 miss → JSC render → store in L1 + write-through to L2 (fire-and-forget).
Invalidation flow (cluster):
- Instance A receives
/api/invalidate→ clears own L1 + L2 entries. - Instance A publishes to
{prefix}invalidatePub/Sub channel. - Instances B, C, D receive message → clear matching L1 entries.
[plugins] (Feature-Gated)
[plugins]
wasm_dir = "plugins" # default: "plugins"
[plugins.analytics]
enabled = false # default: false
export = "json_log" # "json_log" | "prometheus"
sample_rate = 1.0 # default: 1.0
[plugins.security_headers]
enabled = false
csp = "default-src 'self'; script-src 'self' 'nonce-{nonce}'"
csp_report_only = false
[plugins.security_headers.headers]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
[plugins.edge_rewrites]
enabled = false
[[plugins.edge_rewrites.rules]]
source = "/old-path"
destination = "/new-path"
rule_type = "redirect" # "redirect" | "rewrite"
status = 301
[[plugins.edge_rewrites.experiments]]
name = "homepage-v2"
cookie = "ab-homepage"
[[plugins.edge_rewrites.experiments.variants]]
name = "control"
weight = 50
path = "/"
[[plugins.edge_rewrites.experiments.variants]]
name = "variant"
weight = 50
path = "/v2"
[plugins.image_optimization]
enabled = false
allowed_widths = [320, 640, 768, 1024, 1280, 1920]
default_quality = 80
max_source_bytes = 10_485_760 # 10MB
cache_max_entries = 1000
cache_ttl_ms = 3_600_000 # 1h
# WASM plugins
[[plugins.wasm]]
name = "my-plugin"
path = "plugins/my-plugin.wasm"
priority = 1000
[plugins.wasm.permissions]
allowed_urls = ["https://api.example.com/*"]
max_fetch_per_minute = 60
storage_quota_kb = 1024
[plugins.wasm.config]
api_key = "${MY_PLUGIN_API_KEY}" # env var substitution
platform.toml — Multi-App Platform Configuration
[platform]
listen = "0.0.0.0:3000"
data_dir = "./data" # SQLite DB, builds, plugin storage
preview_ttl = "7d" # Preview deploy auto-expiry
max_apps = 50
max_isolates = 100
# TLS termination
# tls_cert = "/path/to/cert.pem"
# tls_key = "/path/to/key.pem"
# tls_auto = true # Auto-provision via ACME/Let's Encrypt
# Default app for unmatched hostnames
# default_app = "marketing"
# Global rate limit (in addition to per-app limits)
[platform.rate_limit]
enabled = true
rpm = 10_000 # Global requests per minute
# Global plugins (applied to all apps)
[platform.plugins]
enabled = ["analytics", "security-headers"]
# App definitions
[apps.marketing]
source = "./apps/marketing"
domains = ["example.com", "www.example.com"]
runtime = "ssr"
[apps.marketing.cache]
default_ttl = "1h"
max_entries = 5000
[apps.marketing.rate_limit]
rpm = 600
[apps.marketing.isolate]
workers = 4
memory_limit = "128MB"
execution_timeout = "5s"
[apps.marketing.plugins]
enabled = ["analytics", "security-headers", "og-image"]
[apps.marketing.deploy]
keep_versions = 3
canary_enabled = true
# Hook scripts
[apps.marketing.hooks]
pre_build = "scripts/pre-build.sh"
post_deploy = "scripts/notify-slack.sh"
[apps.dashboard]
source = "./apps/dashboard"
domains = ["app.example.com"]
runtime = "ssr"
auth.required = true
auth.jwt_secret_env = "DASHBOARD_JWT_SECRET"
[apps.api]
source = "./apps/api"
domains = ["api.example.com"]
runtime = "js"
rate_limit.rpm = 1000
cache.default_ttl = "0" # No caching for API
[apps.docs]
source = "./apps/docs"
domains = ["docs.example.com"]
runtime = "static"
cache.default_ttl = "24h" # Long cache for static docs
bext.plugin.toml — Plugin Manifest
Shipped inside the plugin project, read by bext plugins inspect.
[plugin]
name = "rate-limiter"
version = "1.0.0"
description = "Per-user rate limiting with configurable windows"
author = "Your Name"
license = "MIT"
repository = "https://github.com/you/bext-rate-limiter"
[capabilities]
middleware = true # Implements on_request/on_response
lifecycle = true # Implements on_server_start, etc.
transform = false # Does not transform source code
cache_backend = false # Does not provide L2 cache
[permissions]
kv_namespaces = ["rate-limits"]
max_fetch_per_minute = 0 # No external HTTP needed
storage_quota_kb = 512
secrets = []
cache_read = true
cache_write = false
[config]
# Schema for plugin config (validated on install)
[config.schema]
rpm = { type = "integer", default = 100, description = "Requests per minute per user" }
window_ms = { type = "integer", default = 60000, description = "Rate limit window" }
Environment Variable Substitution
All config values support ${ENV_VAR} substitution:
[auth]
jwt_secret = "${JWT_SECRET}"
[database]
url = "${DATABASE_URL}"
[[plugins.wasm]]
[plugins.wasm.config]
api_key = "${ANALYTICS_API_KEY}"
Substitution happens at config load time. Missing variables cause a startup error (unless the field is optional).
Implementation Tasks
CF-1: Config Parser Enhancements
Tasks:
- Add
[runtime]section toServerConfig - Add
[transforms]section with per-transform config - Environment variable substitution in all string values
- Duration parsing:
"1h","30m","5s","100ms" - Size parsing:
"128MB","1GB","512KB" - Config validation with helpful error messages
-
bext config validateprints resolved config -
bext config diffshows differences from defaults
CF-2: Platform Config Parser
Tasks:
- Create
PlatformConfigstruct - Parse
platform.tomlformat - Per-app config with defaults from platform level
- App config inheritance (global plugins + app-specific)
- Validation: no overlapping domains, valid paths
- Live reload: re-parse on SIGHUP, apply changes
CF-3: Plugin Manifest
Tasks:
- Define
bext.plugin.tomlformat (PluginManifeststruct with serde inbext-plugin-api/src/types.rs) -
bext plugins inspect <path.wasm>reads embedded manifest - Config schema validation on plugin install
- Permission verification against platform config