bunderstackbunderstack/ docs

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)
  }
}

On this page