Environment Validation
Standard Schema validation for server and browser environment values
Bunderstack validates environment values before the application starts.
Server-only and public values stay separate, while app.env and procedure
contexts remain fully typed.
Configure schemas
Each entry accepts Standard Schema. This example uses Valibot:
import * as v from 'valibot'
export const env = {
server: {
STRIPE_API_KEY: v.string(),
WEBHOOK_SECRET: v.string(),
},
client: {
PUBLIC_APP_URL: v.pipe(v.string(), v.url()),
PUBLIC_SENTRY_DSN: v.optional(v.string()),
},
}
export const backend = bunderstack({
schema,
env,
database,
})
export const app = await backend.start()Server keys must not start with PUBLIC_; browser-safe keys must. Naming
violations and invalid values produce a BunderstackEnvError with all issues,
not only the first one.
Describe keys for deployment
Hosting platforms read the committed blueprint to work out what an environment
needs before the first deploy. meta adds value-free metadata per key:
export const env = {
server: {
STRIPE_API_KEY: v.string(),
LOG_LEVEL: v.optional(v.string()),
},
client: {
PUBLIC_APP_URL: v.pipe(v.string(), v.url()),
},
meta: {
STRIPE_API_KEY: { description: 'Secret key from the Stripe dashboard' },
LOG_LEVEL: { sensitive: false, description: 'debug | info | warn | error' },
},
}Server keys are treated as secrets by default and client keys never are — a
PUBLIC_* value is compiled into the browser bundle, so declaring one sensitive
is an error. Descriptions are static prose, at most 200 characters.
Values never reach the blueprint. Only the key name, whether it is required, its scope, its secrecy, and its description do.
Read validated values inside the declaration
The declaration stays one object. The slots that hold credentials —
database, storage, messaging, and realtime — also accept a function of
the validated environment, and auth has taken a builder over { db, env }
since 0.22. Nothing else needs the environment: a procedure or a job reads
ctx.env.
export const backend = bunderstack({
schema,
env,
database: { adapter: libsql() }, // url comes from DATABASE_URL
auth: ({ env }) => ({ baseURL: env.PUBLIC_APP_URL }),
messaging: (env) => ({ email: resend({ apiKey: env.RESEND_API_KEY }) }),
})Those functions are pure and may run more than once — once per inspect(),
once per start(), once per test fixture — so they must not open connections,
read files, or hold state. Two fixtures with different values stay independent
because each resolves the declaration for itself.
A slot supplies a value, never a shape. backend.inspect({ env }) resolves the
declaration and returns its manifest without any I/O, and blueprint generation
resolves it against two probe environments and rejects a declaration whose
shape differs between them:
// Rejected: the blueprint would differ per environment.
bunderstack({
schema,
env,
database,
realtime: (env) => env.FEATURE_FLAG === 'enabled',
})The error names the differing path, such as realtime.required, and never the
probe values. That check is what lets a host provision from the committed
blueprint alone, before any value exists.
Use validated values
app.env.STRIPE_API_KEY
app.env.PUBLIC_APP_URL
api: (o) => ({
publicConfig: o.public.handler(({ context }) => ({
appUrl: context.env.PUBLIC_APP_URL,
})),
})Browser values
createClientEnv() validates only the client section. Server keys become
runtime traps if code tries to access them in a browser bundle.
import { createClientEnv } from 'bunderstack/env'
import { env } from './env-schema'
export const clientEnv = createClientEnv({
...env,
runtimeEnv: import.meta.env,
})Built-in values
Bunderstack also understands database, auth, Redis, messaging, and storage
variables used by its own facilities. Production requires a secure
AUTH_SECRET. Database URLs have adapter-specific development defaults; Redis
is required when separate worker processes publish realtime changes.
import { BunderstackEnvError } from 'bunderstack'
try {
await bunderstack({
schema,
env,
database,
}).start()
} catch (error) {
if (error instanceof BunderstackEnvError) {
console.error(error.issues)
}
}