Skip to content

The Declarative Contract ​

Everything Nanokit does starts from a single file: nanokit.yml. It is not a template or a script generator — it is the complete description of desired state, and the only thing an agent (or a human) needs to write to change infrastructure.

yaml
name: nanokit
activeEnv: local

env:
  SMTP_HOST: '${SMTP_HOST:-service://mail}'

databases:
  nk_db_postgres:
    engine: postgres
    mode: local
    rootPassword: '${DB_PASSWORD_POSTGRES}'
    branching:
      enabled: true

services:
  api-gateway:
    runtime: node
    path: ./apps/api-gateway
    start: npm run start
    expose: 4000
    updateStrategy: recreate
  docs:
    runtime: static
    path: ./dist/apps/docs
    expose: 80

Three things in that example matter to an agent:

  • References, not values. ${DB_PASSWORD_POSTGRES} and service://mail resolve at apply time — from the environment or the secrets vault. Secrets never enter the file, so an agent can write config without ever handling credentials.
  • Declarations, not steps. There is no "restart after env change" instruction. The reconciler derives what must happen from the difference between this file and reality.
  • One file, many targets. environments override slices of this document per context; deploy targets decide where a context lands. Same contract, laptop or cloud.

From file to outcome ​

When the config changes (or nkapp up / nkapp plan runs), Nanokit executes a deterministic loop:

  1. Observe — query the active provider for current state.
  2. Diff — compare desired vs. current, gated by drift hashes so unchanged resources are provably skipped.
  3. Plan — produce a typed plan of CREATE / UPDATE / DELETE / NO_OP actions, ordered by the dependency graph.
  4. Execute proportionally — touch only what the diff justifies; health checks gate each step.
  5. Report — expose resulting state (see Machine-Readable State).

The properties an agent can rely on:

PropertyGuarantee
DeterminismSame config + same current state ⇒ same plan, every time
IdempotenceRe-applying is a no-op when drift hashes match
ProportionalityA one-service change produces a one-service deploy
ReviewabilityEvery decision is a YAML diff a human can read before it executes

Two properties that are commonly assumed and are not guaranteed:

"Constrained scope" is about shape, not authority

The schema constrains what can be declared, not what can be executed. The contract itself expresses free-form shell in six places — services.*.start, services.*.command, services.*.hooks.*, provisioning.install, cron.*.command, and deploy.*.prepare (which runs on the remote host). An actor able to write nanokit.yml and apply it can run commands on the host and on every deploy target.

Bounding that is the job of the agent tool layer, not the config. The MCP server is that layer: an agent's write to any of these fields is refused until a human approves that exact request.

Rollback is manual and one step deep

nkapp deploy --rollback restores the previous reconciler state — the recorded container image and environment — from a local backup state file. It is not automatic: a failed apply does not trigger it. It requires that backup file to exist. And it does not revert code on the remote host or restore database contents; for data, use nkapp backup / nkapp restore.

What this means in practice ​

An agent that needs "a staging environment with Redis" does not run twelve commands. It edits the contract — add the database block, maybe an environment override — and lets reconcile do the rest. If the edit was wrong, nkapp plan shows it before execution.

The full schema (services, databases, runtimes, networks, cron, volumes, infra providers) is documented under Configuration.

Typed primitives on top of the contract

An agent does not have to edit YAML by hand. The MCP server offers typed intent tools: create_environment, create_service, create_database, create_domain, set_secret, update_service and the matching deletes. They write this same file through a closed field allow-list and return the YAML diff of every change. Editing nanokit.yml directly and invoking the CLI keeps working as before.