Skip to content

Server or client?

Layerfig ships two builders. They share a name and an API, but they solve different problems.

Use @layerfig/config — the server builder — unless the configuration has to be readable from code running in the browser. Then use @layerfig/config/client for that subset, and keep the server one for everything else.

@layerfig/config @layerfig/config/client
Reads config files FileSource
Plain objects ObjectSource ObjectSource
Environment variables EnvironmentVariableSource EnvironmentVariableSource
Custom parsers
Reads from process.env import.meta.env
Zod flavour classic Mini
Module formats ESM + CJS ESM only

FileSource uses node:fs and node:path, and absoluteConfigFolderPath is resolved from process.cwd(). None of that exists in a browser. Rather than shipping a build that breaks when bundled, the client entry point simply doesn’t include it — and addSource rejects a FileSource with a clear message if one reaches it anyway.

The client build also swaps classic Zod for Zod Mini, which tree-shakes considerably better. That is the whole reason the two z instances differ.

Often not. If you only need a couple of public values, a schema and a literal object do the job with no extra dependency:

// client-config.ts
import { z } from "zod/mini";

export const config = z
  .object({ appURL: z.url() })
  .parse({ appURL: import.meta.env.PUBLIC_APP_URL });

The client builder earns its place when you want the things Layerfig actually adds:

  • Layering — a base object with per-environment overrides on top.
  • Slots${PUBLIC_API_URL::-http://localhost:3000}, including fallbacks and self-references.
  • One schema — deriving the client shape from your server schema so the two can’t drift. See Client Setup.

If none of those apply, the four lines above are the better answer.

The common arrangement is three files: a shared schema, a server config, and a client config that picks from the schema.