Your first environment
This walkthrough builds one Argo CD Application from scratch using the pieces in examples/full-stack. You will write a module that emits a ConfigMap, compose it into an env file, and run the three CLI commands against it: build, validate, and diff. It assumes you completed Installation and have a konfig.json with at least one env entry.
-
Define a module
A module is a function from options to manifests. You wrap that function with
Module.fixedNswhen the namespace is baked into the module, orModule.dynamicNswhen each instance chooses its own. Settingtarget: Application.targettells the wrapper to produce an Argo CD Application, so each instance becomes its own Application with its own directory in the output. The smallest module in the example isfeature-flags.ts:examples/full-stack/infra/modules/feature-flags.ts import { Application, Sync } from "@konfig.ts/argocd" import { ConfigMap } from "@konfig.ts/k8s" // `ConfigMap.make` infers a literal key union from `data`, so renaming a key here fails // type-check at every `EnvVar.configMapEnv` / `EnvVar.fromConfigMap` call site. export const featureFlags = ConfigMap.make({ name: "feature-flags", namespace: "app", data: { NEW_UI: "true", BETA_DASHBOARD: "false", DARK_MODE: "true" } }) export const defineFeatureFlags = Application.module({ namespace: "app", annotations: Sync.wave(-1), build: (ctx, opts: Record<never, never>) => [featureFlags] })The
buildfunction receives a build context with the instance’snameandnamespace, plus the instance options, and returns manifests. This module has no options of its own, so callers pass onlynameandsource. Theannotations: Sync.wave(-1)line puts the Application in an early sync wave, so consumers that read the ConfigMap start after it exists. -
Instantiate it in an env file
An env file is a plain TypeScript module whose default export is
AppOfApps.fromModules({ ... }). It does three things: builds asourcehelper for the Argo CD repo path, instantiates each module with anameand thatsource, and lists the instances inAppOfApps.fromModules. Frominfra/envs/staging.ts:examples/full-stack/infra/envs/staging.ts const branch = "main" const rootPath = "./infra/k8s/manifests/staging" const src = (name: string) => ({ repoURL: cluster.repositoryUrl, targetRevision: branch, path: `${rootPath}/${name}` })examples/full-stack/infra/envs/staging.ts const featureFlags = defineFeatureFlags({ name: "feature-flags", source: src("feature-flags") })nameis a literal string and becomes both the Application name and the output directory.sourceis the Argo CDsourceblock for the child Application. ItsrootPathmust be the repository path where the rendered tree for this env is committed, because that is where Argo CD will look. In the example it is./infra/k8s/manifests/staging, a committed copy of the.generated/manifests/stagingoutput.The default export composes every instance:
examples/full-stack/infra/envs/staging.ts export default AppOfApps.fromModules({ target: { repoURL: cluster.repositoryUrl, branch, rootPath }, defaults: { destination: { server: "https://kubernetes.default.svc" } }, modules: [ sopsOperator, imagePulls, featureFlags, postgres, apiBuild, workerBuild, redisCache, api, worker ] })targetdescribes the parent app-of-apps source.defaultsis merged into every child Application (here the destination cluster).modulesis the list of instances. If any instance needs something that no other instance provides, this call is a TypeScript error; see Dependency graph. To start with only the ConfigMap, keepmodules: [featureFlags]and add the rest as you go. -
Register the env
Add the entry file to
konfig.json:konfig.json {"root": ".","envs": { "staging": { "entry": "infra/envs/staging.ts" } },"outDir": { "manifests": ".generated/manifests" }}Envs not listed under
envsfall back to<root>/env/<name>.ts. -
Render with
konfig buildTerminal window konfig build stagingThe CLI loads the entry file and fails with
EnvLoadErrorif the default export is not an Effect program. It then renders every Application concurrently, bounded at 4 to keephelmandsopssubprocess counts down. All files are staged under<outDir>.tmpfirst and swapped into place at the end, so an interrupted build never leaves a half-written directory. The output layout is:.generated/manifests/staging/ # <outDir.manifests>/<env>[/<cluster>]apps/ # the app-of-apps (`AppOfApps.fromModules({ name })`, default "apps"): one Application CR per module instanceApplication-feature-flags.yamlApplication-postgres.yamlfeature-flags/ # one directory per Application, named after `name`ConfigMap-feature-flags.yamlpostgres/Namespace-app.yamlStatefulSet-postgres-postgresql.yaml # Helm output is split into one file per documentEvery file is named
<Kind>-<metadata.name>.yaml, with.and/in kind and name replaced by-. Helm releases render throughhelm templateand are split on real YAML document boundaries, so each chart resource lands in its own file too. Passing--cluster <name>adds a cluster segment after the env directory.Builds are cached. The cache key is a hash of everything under
root, stored in.konfig/cache/next tokonfig.json. A second run with unchanged sources and an untouched output tree reports the env as cached, with the file count and output directory, and rewrites nothing.--no-cacheforces a render, and--log jsonswitches the report to one JSON line. Extra inputs outsiderootcan be added to the hash withcacheIncludeinkonfig.json. -
Validate
Terminal window konfig validate stagingkonfig validate staging --strictkonfig validaterenders in memory and checks every document’s envelope. Failures are listed per file and the command exits withStructuralValidationFailed. The checks are:apiVersionandkindare present.metadata.namefollows the RFC 1123 subdomain rule, or the stricter RFC 1123 label rule forNamespaceandService.metadata.namespace, when present, is an RFC 1123 label.labelsandannotations, when present, have string values.
With
--strictit stages a fresh in-memory render into a scratch directory and additionally runskubeconform -summary -strictover it; runningkonfig buildfirst is not required. If the binary is missing the command fails withKubeconformNotFound. Two flags are forwarded to kubeconform:--ignore-missing-schemasfor CRDs it does not know, and--k8s-version, passed as-kubernetes-version. -
Diff against a baseline
Point
diff.baselineinkonfig.jsonat a manifest tree relative toroot, for example the copy committed for Argo CD. The env name is appended, sostagingreads<root>/<baseline>/staging. Then:Terminal window konfig diff stagingkonfig diff staging --format detailThe diff is structural. It parses both sides as YAML, matches documents by kind and name so key order and multi-document ordering do not matter, redacts
Secretdata, and reports only real changes.--formatacceptssummary(default),detail, orjson. A clean run reports that the env matches the baseline. Differences exit non-zero withDiffNonEmpty, which makes the command usable as a CI gate.
Next
- Dependency graph: how declared needs and provides turn a missing Secret into a compile error.
- The Concepts section covers env contracts (defining, binding, and decoding them at runtime),
Module.fixedNsversusModule.dynamicNs, and secret backends.