Skip to content
Docs menu / env

@konfig.ts/env

@konfig.ts/env lets you declare a pod’s environment once and use that one declaration from both sides of the wire. On the manifest side, the declaration produces the Deployment’s env block (through Environment.bind in @konfig.ts/k8s). On the process side, the same declaration decodes the real environment at startup (through Environment.runtime, an Effect Config over the whole tree). Because both sides share one value, renaming DATABASE_URL in one place makes the typechecker flag every consumer that still uses the old name.

Install

Terminal window
bun add @konfig.ts/env

Usage

The API contract from the full-stack example. Its secret entries are Secret.define values shared with the worker, and its literal entries may carry a Config schema so they decode into typed values.

shared/env-contracts/src/bundles.ts
export const apiEnv = Environment.define({
  db: dbCreds,
  s3: s3Creds,
  jwt: jwtKey,
  http: Environment.define({
    port: Literal.define({
      envName: "HTTP_PORT",
      value: 8080,
      schema: Config.Number("HTTP_PORT").pipe(Config.withDefault(8080))
    }),
    logLevel: Literal.define({
      envName: "LOG_LEVEL",
      value: "info",
      schema: Config.String("LOG_LEVEL").pipe(Config.withDefault("info"))
    })
  }),
  runtime: Environment.define({
    nodeEnv: Literal.define({ envName: "NODE_ENV", value: "production" }),
    podName: Downward.define({ envName: "POD_NAME", fieldPath: "metadata.name" })
  })
})

On the process side, Environment.runtime(apiEnv) yields a typed record: config.http.port is a number and config.db.password is Redacted. On the manifest side, Environment.bind({ env: apiEnv, namespace, secrets: { db: { backend } } }) returns the envVars for the container plus the manifests the chosen secret backend emits.

Surface

ExportPurpose
Secret.defineA secret contract { name, namespace, env }; bound to a backend at compose time
Literal.defineA constant { envName, value, schema?, serialize? } baked into the manifest; serialize is required for non-primitive values
Downward.defineA downward-API field { envName, fieldPath }
Environment.defineA nestable bundle of the above; the single source of truth
runtimeDecode an Environment from the process env into an EnvironmentShape
environmentLayerWrap the decoded shape as a Layer for a service
SecretSourcePlaintext sources for backends: fromConfig({ keys, envName? }), literal({ data }), fromCommand({ keys, run })
EnvClaim, HasEnvClaims, EntryKind, EntryMarkerMetadata types used to detect env-name collisions
AnyEnvironment, EnvMember, MemberValue, EnvironmentShape, SecretEntry, LiteralEntry, DownwardEntry, ResolvedSecretValuesTypes for generic helpers over contracts

Errors

ErrorWhen it is raised
EnvNameCollisionTwo members of one bundle claim the same envName. Environment.define catches most collisions at compile time; the remaining cases throw this tagged error synchronously when the bundle is defined (at module load), not inside an Effect
SecretSourceErrorA SecretSource failed to resolve: the fromConfig env var is missing, the fromCommand process exited with a failure, or Sops.source could not decrypt or extract. Carries source, key, and cause

Requirements

  • effect@^4.0.0-rc.111 as a peer dependency (Effect 4, release-candidate line).
  • Depends on @konfig.ts/core.
  • Runtime: Bun recommended; Node >= 23.6 works; Node 22.6 to 23.5 with --experimental-strip-types; tsx works.

Source and README: packages/env.