Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started with Nivis

A hands-on walkthrough using the in-repo fake providers. Everything here runs offline: no provider registry, no cloud account, no credentials. You need Go 1.22+ and Nix.

1. Get the binaries on your PATH

Enter a shell with nivis and the in-repo fake providers, in one command:

nix shell .#nivis .#fake-providers

provider-alpha and provider-beta are minimal tfprotov6 providers used as a hermetic test substrate. Their outputs are a deterministic function of inputs (a per-process counter seeded by TERRAE_NIVIS_FAKE_COUNTER, default 0), so every run is reproducible. The example configs reference them by bare name, so once they are on your PATH there is nothing to build or copy.

Contributors who'd rather use the Go toolchain directly can build instead: go build -o bin/provider-alpha ./cmd/provider-alpha (and provider-beta, nivis). If you do, prepend ./bin to your PATH (export PATH=$PWD/bin:$PATH) so the nivis and bare-name provider sources resolve just as in the Nix shell.

Scaffold a tutorial with nivistutor

If you do not have a repo checkout and just want to try Nivis in a sandbox, nivistutor writes a tutorial's starter files (a ready flake.nix, the config, and a README) into a directory of your choosing, so you run it with plain nivis, with no --flake/--attr flags. It scaffolds the files for you to read and run; it does not run nivis for you (you learn by doing).

nix shell github:nivis-project/nivis#nivis github:nivis-project/nivis#tutor
nivistutor

It greets you, lists the available tutorials (this getting-started one and the current release's features tutorial), asks whether to write into a new subdirectory or the current one, writes the files, and prints the exact nivis commands to run next. The #tutor shell carries the fake providers too, so the scaffolded project runs immediately. Non-interactively:

nivistutor --list                                   # the available tutorials
nivistutor --tutorial getting-started --dir my-nivis # write without prompts

The starter's flake.nix is pinned to the nivis release that scaffolded it, so the library and your nivis binary agree. Existing files are never overwritten without --force.

2. The example configuration

The flake's nivis.plan (in nix/example/) describes three resources and a consumer, wired so each hop crosses the Nix boundary:

alpha_token.A            (alpha)            -- no inputs; A.value computed at apply
   └─ name = "rec-" + A.value               (a __derived value)
beta_record.B  (beta)    from = name        -- B.endpoint computed at apply
   └─ final = B.endpoint + "::" + A.value    (a __derived on BOTH providers)
alpha_token.C  (alpha)   label = final

systemConfig (a Nix consumer) reads:
  recordEndpoint = B.endpoint   # from beta
  tokenValue     = A.value      # from alpha
  combined       = final        # from both

Because name and final are values Nix computes from provider outputs, they can't be known until those outputs exist and Nix is re-evaluated, which is what forces multiple phases.

3. Plan and apply

nivis plan
  + alpha.alpha_token.A (alpha_token)
  + beta.beta_record.B (beta_record)
  + alpha.alpha_token.C (alpha_token)

3 change(s) across 3 resource(s) (+ create, ~ update, -/+ replace, = no change). Run `nivis apply`.
nivis apply
Applied 3 resource(s) across 3 phase(s):

Phase 1
  + alpha.alpha_token.A
Phase 2
  + beta.beta_record.B
Phase 3
  + alpha.alpha_token.C

Three phases, not one: phase 1 applies A (nothing else is ready); re-evaluating with A.value known unlocks B; re-evaluating with B.endpoint known unlocks C. The loop halts at a fixpoint once nothing new resolves.

Provider notes

Providers log about their own internals while they work. Nivis surfaces what they say as provider notes — one line each, next to (never inside) the change list:

provider note aws_amplify_app.description: unable to require attribute replacement (detail: ForceNew: No changes for description)

Read that as: while planning an aws_amplify_app, the provider mentioned something about the description attribute. A note is not a failure. The detail: part is the provider's own explanation of why it said something — in this example, the AWS SDK considered forcing a replacement of description and decided against it, because nothing changed. Providers report that as an "error" field internally; Nivis renders it as a detail precisely so it does not read as one. If a run actually fails you get an error: line and a non-zero exit status, not a note.

A note that recurs — many providers log the same internal remark once per resource — is printed once, and the end of the run reports the rest:

provider note aws_amplify_app.description: unable to require attribute replacement (11 further occurrence(s) not shown)

Notes go to stderr, while the change list goes to stdout, so nivis plan > plan.txt keeps the change list unmixed.

To see more or less of this, use --provider-log-level:

ValueWhat you get
offnothing from providers at all
erroronly provider errors — no notes
warn (default)provider warnings as notes
info, debugprogressively more, still rendered as notes
tracethe provider's entries unabridged, fields included

trace is the setting for debugging a provider or filing a provider bug report: it restores the request ids, caller sites and RPC names that a note deliberately drops. Expect a lot of output — providers are verbose at that level.

The flag governs what you see. It is separate from TF_LOG, which the provider process inherits from your environment and which governs how much the provider emits: setting TF_LOG=trace cannot flood a run whose --provider-log-level is warn.

Note that a plan of a stack that is not yet in state reports creates without contacting the provider, so it produces no provider notes. Notes appear from apply onward, and on a re-plan of a stack that already has state.

--provider-log-level governs the providers. For Nivis's own progress reporting — how much it tells you while a long run is working, which channel each kind of output goes to, and the colour and terminal rules — see Reading Nivis output.

4. Inspect the round trip

nivis state list
nivis state show alpha.alpha_token.C
alpha.alpha_token.C (alpha_token)
  id = alpha-1
  label = beta://rec-alpha::0::alpha::0
  value = alpha:beta://rec-alpha::0::alpha::0:1

C.label is final: a string Nix built from both B.endpoint (beta) and A.value (alpha). That value only became concrete after both providers applied and Nix re-evaluated. That is the round trip.

Stack outputs

To surface named values out of a run (the Terraform output "x" {} equivalent), declare them with the outputs argument to toIR:

toIR {
  providers = { ... };
  resources = [ A B C ];
  outputs = {
    token = A.refAttr "value";          # from one resource
    combined = final;                   # composed across both providers
  };
  inherit ledger;
}

Read them after apply with nivis output (resolved from current state):

nivis output
combined = beta://rec-alpha::0::alpha::0
token = alpha::0

nivis output <name> prints a single value, and nivis output --json prints a JSON object ({ "<name>": <value> }) for a CI step or another stack to consume. Outputs reuse the round trip's resolution, so a value composed across providers and phases comes back concrete.

5. Refresh and destroy

nivis refresh    # reconciles state via ReadResource; no changes here
nivis destroy    # tears down in reverse dependency order
Destroyed 3 resource(s):
  - alpha.alpha_token.C
  - beta.beta_record.B
  - alpha.alpha_token.A

6. Generate constructors from a provider schema

nivis gen turns any provider's schema into typed Nix constructors:

nivis gen --provider provider-alpha --out ./generated
cat ./generated/alpha/alpha_token.nix

The generated constructor requires the provider's required inputs (throwing a named error if missing), passes optional inputs through, omits computed-only attributes (they're outputs), and accepts an overrides argument so you can adjust the generated output.

It also includes the resource's nested blocks, each with the shape that matches its nesting, so you never guess list-vs-single: a list/set-nested block (e.g. ingress, disk_container) is an argument defaulting to [] and written [ { ... } ]; a single-nested block is a plain attrset; a map-nested block is a map of attrsets. Every block is documented in the generated file with its nesting and inner attribute names.

So the generated constructor is your per-provider argument reference: instead of translating a provider's Terraform docs into Nivis terms by hand, run nivis gen and read the .nix. It lists every argument (with type and required/optional), every nested block (with its nesting), and the computed outputs, all in Nivis form.

7. A real provider (AWS)

Everything above is offline against the fakes. The same nivis commands drive real providers: nivis resolves a provider by address from the OpenTofu registry, downloads and checksum-verifies the binary, negotiates the plugin protocol (AWS speaks v5), configures it, and runs plan/apply/destroy. The example nix/example/aws.nix (flake attr nivis.aws) declares the hashicorp/aws provider with mkProvider and one aws_s3_bucket.

⚠️ This creates a real resource in your AWS account: one (free-tier) S3 bucket, then destroys it. The provider's region lives in the Nix config; only credentials come from the environment (the AWS SDK default chain), so set AWS_PROFILE (or AWS_ACCESS_KEY_ID/…). The first run downloads the ~900 MB AWS provider (cached afterwards).

For the full, hand-held walkthrough (prerequisites, writing the config line by line, plan/apply/inspecting state/destroy, and troubleshooting) follow the AWS S3 tutorial.

Where to go next

  • IR-CONTRACT.md + ir-schema.json: the IR, the stable contract between the Nix frontend and the Go executor.
  • TESTING.md: the test layers and the headline two-provider e2e.
  • DESIGN.md: why the architecture is the way it is (spawn-not-link, batch-not-live, phased re-eval to a fixpoint).

The core test suite is hermetic (fakes, no network/credentials); real-provider support (registry download + checksum verification, tfprotov5/6) is proven against AWS as shown above.