Skip to content
LogoLogo

extends

extends reuses an EnvironmentSchema object — a plain object with the same shared/server/client/clientPrefix shape createEnvironment accepts, minus the runtime-specific options. This works like tsconfig.json's extends, which reuses a base config.

A shared package exports a plain object satisfying EnvironmentSchema:

// packages/db/src/env-schema.ts
import type { EnvironmentSchema } from "@zap-studio/env";
import { z } from "zod";
 
export const dbEnvironmentSchema = {
  server: { DATABASE_URL: z.string().url() },
} satisfies EnvironmentSchema;

An app composes it in via extends:

// apps/api/src/env.ts
import { createEnvironment } from "@zap-studio/env";
import { dbEnvironmentSchema } from "@your-org/db";
import { z } from "zod";
 
export const env = createEnvironment({
  extends: [dbEnvironmentSchema],
  server: { PORT: z.coerce.number().default(3000) },
  runtimeEnv: process.env,
});
 
env.DATABASE_URL; // typed and validated, from dbEnvironmentSchema
env.PORT; // typed and validated, from this call's own schema

extends accepts a list, composed in order — typically the extends entries followed by the call's own shared/server/client.

Conflict Detection

If two composed sources declare the same key with two different schemas, createEnvironment throws an EnvironmentError right away, instead of silently letting one overwrite the other:

createEnvironment({
  extends: [{ server: { PORT: z.string() } }],
  server: { PORT: z.coerce.number() }, // ❌ EnvironmentError: "PORT" declared with two different schemas
  runtimeEnv: process.env,
});

There is one exception: when both sources use the exact same schema object reference. This can happen when two packages import one shared constant:

import { PORT_SCHEMA } from "@your-org/shared-schemas";
 
const dbEnvironmentSchema = { server: { PORT: PORT_SCHEMA } } satisfies EnvironmentSchema;
const cacheEnvironmentSchema = { server: { PORT: PORT_SCHEMA } } satisfies EnvironmentSchema;
 
createEnvironment({
  extends: [dbEnvironmentSchema, cacheEnvironmentSchema], // fine — same schema object, no conflict
  runtimeEnv: process.env,
});

Standard Schema has no generic introspection API, so structural schema-equivalence (two different z.string() calls that happen to produce the same rules) can't be reliably detected. Reference equality is the honest, buildable signal for "this is safe to merge."

clientPrefix Per Source

Each composed EnvironmentSchema carries its own clientPrefix, checked independently — see server/client/shared for the full rule.

generateEnvironmentExample Also Accepts extends

extends works the same way for generateEnvironmentExample, so a generated .env.example includes every var from every composed source.

See Also

  • Presets — ready-made EnvironmentSchema objects for hosting platforms, composed the same way
  • Errors — the exact EnvironmentError messages thrown on conflict