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:

  1. Request arrives → check L1 (in-memory). Hit? Serve immediately.
  2. L1 miss → check L2 (Redis). Hit? Populate L1 with remaining TTL, serve.
  3. L2 miss → JSC render → store in L1 + write-through to L2 (fire-and-forget).

Invalidation flow (cluster):

  1. Instance A receives /api/invalidate → clears own L1 + L2 entries.
  2. Instance A publishes to {prefix}invalidate Pub/Sub channel.
  3. 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 to ServerConfig
  • 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 validate prints resolved config
  • bext config diff shows differences from defaults

CF-2: Platform Config Parser

Tasks:

  • Create PlatformConfig struct
  • Parse platform.toml format
  • 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.toml format (PluginManifest struct with serde in bext-plugin-api/src/types.rs)
  • bext plugins inspect <path.wasm> reads embedded manifest
  • Config schema validation on plugin install
  • Permission verification against platform config