Config

@beignet/core/config validates deployment configuration and gives the app a typed env object. Beignet apps should define server-only and client-safe variables explicitly so secrets cannot be read from client code by accident.

bun add @beignet/core

App env

Use createEnv(...) from lib/env.ts:

import { createEnv } from "@beignet/core/config";
import { z } from "zod";

export const env = createEnv({
  server: {
    NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
    DATABASE_URL: z.string().url(),
    LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
  },
  clientPrefix: "NEXT_PUBLIC_",
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnv: process.env,
});

Server variables are available on the server. Client variables must start with clientPrefix. If a server-only key is read through the returned env object in a client runtime, Beignet throws a descriptive error.

Server runtimes validate both server and client variables at startup. Client runtimes validate only client variables, so a public bundle does not need server secrets just to import the shared env object.

Strict runtime env

Some frameworks only bundle environment variables that are explicitly accessed. Use runtimeEnvStrict to make those accesses visible:

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
  },
  clientPrefix: "NEXT_PUBLIC_",
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
  },
  runtimeEnvStrict: {
    DATABASE_URL: process.env.DATABASE_URL,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
});

Every key validated in the current runtime must exist on runtimeEnvStrict, even if its value is undefined. Server runtimes require all declared keys; client runtimes require only client keys. That catches missed destructures during build without forcing server secrets into client bundles.

Empty strings

createEnv(...) treats empty strings as undefined by default. This keeps schema defaults working when .env contains values like:

LOG_LEVEL=

Set emptyStringAsUndefined: false when an empty string should be validated as an actual value.

Prefix stripping

Use defineEnv(...) when you already have a whole-object schema or need prefix stripping:

import { defineEnv } from "@beignet/core/config";
import { z } from "zod";

const appEnv = defineEnv({
  prefix: "APP_",
  schema: z.object({
    DATABASE_URL: z.string().url(),
    SECRET_KEY: z.string().min(1),
  }),
});

export const config = appEnv.load();

This reads APP_DATABASE_URL and APP_SECRET_KEY, strips APP_, validates the resulting object, and returns { DATABASE_URL, SECRET_KEY }.

Testing

Pass a custom env object instead of reading from process.env:

const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
  },
  runtimeEnv: {
    DATABASE_URL: "postgres://localhost/test",
  },
});

Provider config

Provider configuration uses the same Standard Schema helpers internally. A provider can declare an envPrefix, and the server strips the prefix before validating provider config:

import { createProvider } from "@beignet/core/providers";
import { z } from "zod";

createProvider({
  name: "mail",
  config: {
    envPrefix: "MAIL_",
    schema: z.object({
      HOST: z.string(),
      PORT: z.coerce.number().int(),
    }),
  },
  async setup({ config }) {
    // config.HOST and config.PORT are validated
  },
});