Environment Variables
Load .env files, use typed imports, and understand the four-tier prefix system.
.env File Loading
Bosia loads environment variables from .env files in order (later overrides earlier):
.env.env.local.env.[mode](e.g..env.development,.env.production).env.[mode].local
System environment variables always take highest precedence — .env files never overwrite existing system vars.
Names vs values
Those four files decide the values for the current mode. The names are collected from every .env* file in your project root — including .env.production, .env.test and .env.example — so $env exports the same list in dev, production and CI. A name declared only in a file the current mode doesn't load simply has an empty value until something supplies one.
This makes .env* a manifest: a variable whose value comes from your host (Vercel, Docker, CI secrets) still has to be declared in some .env* file — usually .env.example — or it won't exist on $env and type-checking will fail.
Prefix System
Variable names control where and when they're available:
| Prefix | Client | Server | Timing | Example |
|---|---|---|---|---|
PUBLIC_STATIC_ |
Yes | Yes | Build-time | PUBLIC_STATIC_APP_NAME |
PUBLIC_ |
Yes | Yes | Runtime | PUBLIC_API_URL |
STATIC_ |
No | Yes | Build-time | STATIC_BUILD_ID |
| (no prefix) | No | Yes | Runtime | DATABASE_URL |
- Build-time variables are inlined during
bosia build— changing them requires a rebuild - Runtime variables are read from
process.envon each request - Client variables are safely exposed to the browser; Server variables never leave the server
Accessing Variables
Import from the $env virtual module:
import { PUBLIC_API_URL, DATABASE_URL } from "$env";Only variables declared in your .env files are available through this import. The module is type-safe with auto-generated type declarations.
Framework Variables
These variables are reserved by Bosia and control framework behavior. Every name starting with BOSIA_ is reserved too. Reserved names never show up in $env, even when declared in .env:
| Variable | Default | Description |
|---|---|---|
PORT |
9000 |
Server port |
NODE_ENV |
— | development or production |
BODY_SIZE_LIMIT |
512K |
Max request body size (supports K, M, G, Infinity) |
IDLE_TIMEOUT |
10 |
Bun.serve idle timeout in seconds (max 255). Raise when API routes hold streaming responses with long gaps between chunks — e.g. an AI chat endpoint waiting for a tool call that shells out to bunx bosia add. Set to 0 to disable. |
BOSIA_REUSE_PORT |
0 |
Set to 1 to allow several server processes to share one port (SO_REUSEPORT), for deliberate multi-process clustering. Off by default: otherwise a second bosia start silently joins the port and the kernel splits traffic between two different builds. With it off, the second server refuses to start and exits non-zero. |
BASE_PATH |
"" (origin root) |
Mount the whole app under a URL prefix, e.g. /sso. Bosia strips it before routing and puts it back on redirects, framework asset URLs, cookies and the client router, so routes and event.url.pathname stay prefix-free. Requests outside the base 404. Must be set for bosia build as well as bosia start. URLs your own components build need the base export. See Deployment › Mounting Under a Sub-Path. |
MAX_INFLIGHT |
Infinity |
Soft cap on concurrent in-flight requests. When set, requests above the cap get a fast 503 Service Unavailable with Retry-After: 1 before any work is done — protects single-replica container deploys from OOM under traffic spikes. /_health is always exempt so orchestrator liveness probes keep working while the app sheds load. Pick a value below what your container's memory can support per concurrent SSR render (a safe starting point is 500; tune from there). |
WS_MAX_PAYLOAD |
16M |
Largest WebSocket frame accepted (same format as BODY_SIZE_LIMIT, no Infinity). A bigger frame closes the socket. See WebSockets. |
WS_IDLE_TIMEOUT |
120 |
Seconds a WebSocket may go without a frame before Bun closes it (0–960, 0 = never). Bun pings idle sockets itself, so a live peer never hits it. |
WS_BACKPRESSURE_LIMIT |
16M |
Most bytes queued for a slow WebSocket client. Past it, send() drops the message and returns 0; pause on -1 and wait for drain to stay under it. |
WS_COMPRESS |
false |
true turns on WebSocket compression (permessage-deflate). Helps text-heavy streams like terminal output; costs CPU. |
CACHE_KEYS |
session,sid,auth,token,jwt,Authorization |
Comma-separated cookie/header names that contribute to the response cache identity. Pages with different values for any of these names are cached separately so per-user content never leaks. See Response cache. |
CACHE_MAX_ENTRIES |
500 |
LRU capacity of the in-memory response cache. Set to 0 to disable the cache entirely. Each entry holds raw + gzip + brotli bytes for one URL/identity pair. See Response cache. |
CACHE_MAX_BYTES |
104857600 |
Total memory (bytes) for all response cache entries together; the oldest are dropped first. Set to 0 to limit by entry count only. |
CACHE_IGNORE_PARAMS |
utm_*,fbclid,gclid,… |
Query params left out of page cache keys, so ?utm_source=… links get the cached page. name* = prefix; setting it replaces the list; none = off. See Response Cache. |
CACHE_WAIT_TIMEOUT |
60000 |
Ms a cache miss waits for another request building the same entry before building itself, so a hung handler can't block its URL. Also the longest a client-navigation data request joins an identical one in flight (cache on or off). Counted from the first request's start. Defaults to METADATA_TIMEOUT + LOAD_TIMEOUT + 10000 when that is longer. 0 or Infinity = wait for it. |
ASSET_CACHE_MAX_BYTES |
52428800 |
Total memory (bytes) for keeping static files and prerendered pages after their first request. Set to 0 to always read from disk. Bun only; ignored on Cloudflare Workers. See Deployment. |
ASSET_CACHE_MAX_FILE_BYTES |
1048576 |
Largest file (bytes) the asset cache keeps. Bigger files are always read from disk. |
LOAD_TIMEOUT |
30000 |
Timeout for load() in ms; a page's time includes its await parent(). A loader that never finishes is cut off with a 500, so its URL recovers. Bun closes a connection that sends nothing for IDLE_TIMEOUT (10s): raise that too for loaders slower than it. 0 or Infinity = no timeout. |
METADATA_TIMEOUT |
10000 |
Timeout for metadata() in ms. 0 or Infinity = no timeout. |
PRERENDER_TIMEOUT |
5000 |
Timeout for each prerender fetch in ms (a whole number, or the build fails). It is the only limit at build: LOAD_TIMEOUT, METADATA_TIMEOUT, MAX_INFLIGHT and IDLE_TIMEOUT are turned off for the prerender server |
PRERENDER_CONCURRENCY |
6 |
How many prerender requests bosia build runs at once; a page and its data are two, each running load() |
CSRF_ALLOWED_ORIGINS |
— | Comma-separated allowed origins for CSRF |
CSRF_EXEMPT_PATHS |
— | Comma-separated request paths exempt from the CSRF origin check, for server-to-server webhooks that send no Origin/Referer. Matched exact or on a path boundary (/webhook covers /webhook/… but not /webhooky). Exempt routes bypass CSRF, so they must authenticate the caller themselves (verify a webhook token/signature). See Security › Exempting webhook paths. |
CORS_ALLOWED_ORIGINS |
— | Comma-separated allowed origins for CORS |
CORS_ALLOWED_METHODS |
— | Comma-separated allowed methods |
CORS_ALLOWED_HEADERS |
— | Comma-separated allowed headers |
CORS_EXPOSED_HEADERS |
— | Comma-separated exposed headers |
CORS_CREDENTIALS |
false |
Set to "true" to allow credentials |
CORS_MAX_AGE |
86400 |
Preflight cache duration in seconds |
TRUST_PROXY |
false |
Trust X-Forwarded-Host / X-Forwarded-Proto for the public origin: event.url, redirects, CSRF and WebSocket origin checks, Secure cookies. Only enable when behind a reverse proxy that strips client-supplied forwarded headers. See Security › Reverse-proxy deployments. |
DISABLE_X_FRAME_OPTIONS |
false |
Set to "true" to omit the X-Frame-Options: SAMEORIGIN header. Use when the app is intentionally embedded as an iframe by a different origin. See Security › Disabling X-Frame-Options. |
CSP_DIRECTIVES |
— | Opt-in Content-Security-Policy. {nonce} is replaced per request. See Security › Content Security Policy. |
BOSIA_OUT_DIR |
./dist |
Build output directory (read by bosia build + bosia start). Use a custom path to run a verification build without colliding with a running bosia dev — e.g. BOSIA_OUT_DIR=.bosia/verify bun run build. bosia dev ignores this and always writes to .bosia/dev/. |
Framework variables are accessed via process.env directly, not through $env.
Example .env File
# Public — available on client and server at runtime
PUBLIC_API_URL=https://api.example.com
PUBLIC_APP_NAME=My App
# Public static — inlined at build time
PUBLIC_STATIC_VERSION=1.0.0
# Private — server only
DATABASE_URL=postgres://localhost:5432/mydb
# Also supported (see /guides/database):
# DATABASE_URL=mysql://user:pass@host:3306/mydb
# DATABASE_URL=sqlite://./data/app.db
# DATABASE_URL=sqlite://:memory: # dev/test only — flushes on restart
API_SECRET=sk_live_abc123
# Framework config
PORT=3000
BODY_SIZE_LIMIT=1M
CORS_ALLOWED_ORIGINS=https://app.example.comSecurity
Only PUBLIC_* variables declared in .env files are sent to the client. Variables set only as system env vars (not in .env files) are never exposed to the browser, even if they have a PUBLIC_ prefix.