Skip to content

Client Configuration

@layerfig/config/client exports a ConfigBuilder that is safe to bundle for the browser. It accepts a smaller set of options than the server one, because there is no filesystem to read from:

Option Client Default
validate ✅ required
runtimeEnv import.meta.env
slotPrefix "$"
absoluteConfigFolderPath ❌ not available
parser ❌ not available

The accepted sources are narrower too: the client builder takes ObjectSource and EnvironmentVariableSource only. Passing a FileSource throws Invalid source. Client ConfigBuilder only Accepts ObjectSource or EnvironmentVariableSource.

The ConfigBuilder constructor requires a validate function. This function receives two arguments: the final merged configuration and a Zod mini instance (v4). It must return the validated configuration.

// src/config/client.ts
import { ConfigBuilder, ObjectSource } from "@layerfig/config/client";

// config will be type `z.output<typeof schema>` (the actual object type)
export const config = new ConfigBuilder({
  validate: (finalConfig, z) => {
    const schema = z.object({
      appVersion: z.string(),
    });

    return schema.parse(finalConfig);
  },
  runtimeEnv: import.meta.env,
})
  .addSource(
    new ObjectSource({
      appVersion: "${PUBLIC_APP_VERSION}",
    })
  )
  .build();

You can also use other validation libraries. Return the result of the schema validation from the validate function, and the config object will be typed accordingly:

// src/config/client.ts
import { ConfigBuilder, ObjectSource } from "@layerfig/config/client";
import * as v from "valibot";

const configSchema = v.object({
  appVersion: v.string(),
});

export const config = new ConfigBuilder({
  validate: (finalConfig) => v.parse(configSchema, finalConfig),
  runtimeEnv: import.meta.env,
})
  .addSource(
    new ObjectSource({
      appVersion: "${PUBLIC_APP_VERSION}",
    })
  )
  .build();

Default: import.meta.env

The object slots are resolved against. On the client this is almost always import.meta.env, which is what bundlers like Vite replace at build time with the subset of variables your framework exposes to the browser.

runtimeEnv: import.meta.env,

Pass it explicitly. The default is read once when the module is first evaluated, and some bundlers only substitute import.meta.env where it appears literally in your source — naming it in your own file is the reliable form.

Any plain object works too, which is what makes client configs testable:

runtimeEnv: { PUBLIC_APP_VERSION: "1f550b7" },

Default: "$"

A string that identifies placeholders to be replaced with environment variables.

By default, Layerfig looks for placeholders prefixed with $. You can customize this prefix to avoid conflicts or to match a team’s convention.

For example, to use a double underscore (__) as the prefix:

// src/config/client.ts
import { ConfigBuilder, ObjectSource } from "@layerfig/config/client";

export const config = new ConfigBuilder({
  validate: (finalConfig, z) => {
    const schema = z.object({
      appVersion: z.string(),
    });

    return schema.parse(finalConfig);
  },
  runtimeEnv: import.meta.env,
  slotPrefix: "__",
})
  .addSource(
    new ObjectSource({
      appVersion: "__{PUBLIC_APP_VERSION}",
    })
  )
  .build();

Layerfig will now look for placeholders like __{PUBLIC_APP_VERSION}.

Assuming the PUBLIC_APP_VERSION environment variable is set, the resolved configuration will be:

config.appVersion; // 1f550b7