Nivis lets you manage real infrastructure from Nix: Terraform/OpenTofu provider resources are first-class Nix values. You write your infra in a flake; a thin Go executor speaks the provider plugin protocol directly to unmodified provider binaries and applies it. No HCL, no separate state language, Nix is the configuration, the provider does the work.
The capability at the heart of Nivis is the round trip: a provider-created resource returns computed values (an IP, an ID, a generated name) back into Nix, which re-evaluates to produce dependent configuration, repeating to a fixpoint. So a value generated by your cloud can become the input to the next resource, or the body of a file, computed in Nix. (See it end to end in the AWS tutorial.)
The round trip is not unique to Nivis, and we say so. NixOps 4 feeds provider outputs back into Nix too, structural dependencies included. What is particular here is that the Terraform-provider coupling is the whole product: unmodified provider binaries, fetched and checksum-verified from the OpenTofu registry, so every provider works on day one. See docs/COMPARISON.md.
Status: early but real. The round trip works across two providers, and real providers (AWS today) apply/update/replace/destroy. Expect rough edges; the contracts (the IR, the flake interface) are the stable parts.
You need Nix (with flakes). Run Nivis straight from the flake, no checkout:
nix run github:nivis-project/nivis#nivis -- --versionThen point it at a flake that describes your infra. A minimal one:
{
inputs.nivis.url = "github:nivis-project/nivis";
outputs = { self, nivis }:
let lib = nivis.lib; in {
# `nivis` evaluates this attribute by default.
nivis.plan = ledger: lib.toIR {
providers.aws = lib.mkProvider {
source = "registry.opentofu.org/hashicorp/aws";
config.region = "eu-central-1";
};
resources = [
(lib.mkResource {
provider = "aws"; type = "aws_s3_bucket"; name = "demo";
config.force_destroy = true;
})
];
inherit ledger;
};
};
}From that directory:
nivis plan # show what will change (+ create ~ update -/+ replace = no change)
nivis apply # apply, resolving across phases to a fixpoint
nivis state show <resource-id>
nivis destroy(nivis shells out to nix to evaluate your config, and fetches + checksum-
verifies real providers from the OpenTofu registry on first use.)
- New here? The getting-started guide walks the
offline demo (in-repo fake providers, no cloud) and schema codegen (
nivis gen). - Real infra? The AWS S3 tutorial builds a bucket and a file whose contents Nix generates from the bucket's name (the round trip), from an empty directory.
- Installing
nivis(nix shell,nix profile): see docs/INSTALL.md. - How does it compare? An honest comparison with Terranix, OpenTofu/Terraform, Pulumi, NixOps, CDK and CloudFormation, including where Nivis is still young.
- Browse it all on the docs site.
Nix evaluates your configuration to a JSON IR. Values that aren't known until
apply-time are emitted as typed placeholders, a __ref (another resource's
output) or a __derived (a value Nix computed from an output). The executor
ingests the IR, drives the providers, gathers their apply-time outputs into a
ledger, then re-evaluates Nix with that ledger injected so placeholders
resolve; deeper chains take more phases, to a fixpoint. The full reasoning and
why this (not an Output<T> promise model) is the honest, Nix-shaped approach
is in docs/OVERVIEW.md and
docs/DESIGN.md.
nivis.lib is pure (builtins only, no nixpkgs, so it evaluates without the
binary cache):
mkResource { provider; type; name; config; }: a resource, with output references (refAttr) for wiring one resource into another.mkData { provider; type; name; config; }: a datasource (existing infra to read, not create); its outputs feed resources viarefAttr, read per phase. See docs/DATASOURCES.md.mkProvider { source; config; }: a provider, config (incl. nested blocks) in Nix; flows into the provider'sConfigure.str/derived: build values from provider outputs (the round trip).mkVars: declare typed config variables with defaults (required when no default), resolved from--var/--var-file/NIVIS_VAR_*(an explicit--varwins). Read them asvars.<name>in the plan.drv/drvFile: mark a Nix build output (a derivation) sonivis applyrealises it before use;drv dusesd.passthru.filePathwhen present,drvFile d "sub/path"names a file explicitly. (See the EC2 tutorial.)toIR: serialize to the IR;mkResources(count/for_each) andevalModules(module composition).
nivis gen --provider <binary> --out <dir> generates typed constructors from any
provider's schema, including nested blocks (with the right list-vs-single shape).
The generated constructor doubles as the per-provider argument reference.
Versioned interfaces, change the spec before the shape:
- The IR:
docs/IR-CONTRACT.md+docs/ir-schema.json(the normative JSON Schema). Both the NixtoIRproducer and the Go consumer validate against it. - The flake interface:
nivis.plan = ledger → IR, evaluated each phase with the outputs ledger injected.
Nivis is built with the standard Go toolchain; the flake (nix build .#nivis,
nix run .#nivis) is the packaged path. From a checkout:
go build -o bin/nivis ./cmd/nivis # the CLI
go test ./... # Go unit + e2e (uses the in-repo fakes)
bash tests/run-nix-tests.sh # Nix property tests + IR conformance
bash tests/check-docs-ssot.sh # docs single-source-of-truthThe repo follows a spec-driven process guided by openspec/ (the per-change
specs). Releases: docs/RELEASING.md.
Nivis's own code is Apache-2.0 (LICENSE). See LICENSING.md for the
breakdown and NOTICE for attributions.
