What runs where
Everything below runs inside your boundary. Ports are bound to the loopback interface by default; put your own TLS reverse proxy in front of the data plane.
| Service | What it does | Holds prompt text? |
|---|---|---|
dataplaneimage terraflow, port 8080 | The gateway (/v1), the engine and scheduler, the console API (/api) and the console itself (/console). Health at /health and /ready | In memory, while a request runs |
postgresPostgreSQL 16 with pgvector | Contracts and signed bundles, the hash-chained audit ledger, cost records, users, key hashes, documents for retrieval | No. Answers held for review are stored encrypted |
redisRedis 7, append-only persisted | Budgets, rate limits, sessions and the audit outbox | No |
cache-redisRedis 7, persistence off | The answer cache. It must be a separate, non-persisted instance: outside development the plane refuses to start without TERRAFLOW_CACHE_REDIS_URL | Yes, in memory only, never written to disk |
| Ollama (optional) | Small local models for document search and request-type routing, so no request text leaves for that. On with TERRAFLOW_LOCAL_MODELS=ollama and TERRAFLOW_OLLAMA_URL | In memory, while it runs |
otel-collector (optional profile) | OpenTelemetry export; alert rules in deploy/alerts/terraflow.rules.yml | No |
mgmt (optional, connected mode) | Publishes signed releases to connected planes. Never on a request's path | No |
Trust is anchored in files, not in the database: TERRAFLOW_KEYS_DIR holds the plane signing key, the
pinned vendor release key (trusted-keys.json), the bundle version high-water mark and the ledger
anchor. Keep it on its own volume and back it up with the database.
Prerequisites
- A Linux host in your network or cloud account with Docker Engine and the Compose plugin.
- A TLS-terminating reverse proxy in front of port 8080. Outside development the console's session cookie is
Secure, so the console must be served over HTTPS. SetTERRAFLOW_FORWARDED_ALLOW_IPSto the proxy's address so sign-in throttling sees real client addresses. - For an offline install: the
terraflowCLI installed from the release wheel delivered with the bundle (the verifier must not come from the images it verifies), and the vendor release key's public half, received out of band. - Accounts for the models you will allow: your own GPUs behind an OpenAI-compatible server, Azure OpenAI, Amazon Bedrock, the Anthropic API or another OpenAI-compatible endpoint.
- Optional: Ollama on the host or in your network for the local helper models.
The kit is Docker Compose on a VM. A Kubernetes package is not part of the current kit; the plane is one container image that uses external PostgreSQL and Redis, so it can be scheduled there with your own manifests.
Settings and secrets
Set these in the environment of a real installation. The Compose files fall back to development-only values, which the plane refuses outside development.
| Setting | Value |
|---|---|
TERRAFLOW_ENV | pilot in the offline stack, or production |
TERRAFLOW_DB_PASSWORD | PostgreSQL password |
TERRAFLOW_REDIS_PASSWORD | Password for both Redis instances |
TERRAFLOW_SESSION_SECRET | At least 32 characters; never the published default |
TERRAFLOW_CACHE_REDIS_URL | The non-persisted cache Redis |
TERRAFLOW_LOCAL_MODELS | simulated (default) or ollama |
TERRAFLOW_PRIVATE_CIDRS | Your private network ranges. A model marked private whose endpoint resolves outside them is refused when rules are activated |
TERRAFLOW_SEED_PROFILE | demo, preview or none: what a new installation starts with. Agree it with your TerraFlow engineer |
Install from the signed bundle
The offline bundle carries TerraFlow and its third-party images to a host with no network. Nothing is pulled or built on the host.
- Pin the vendor release key in
keys/trusted-keys.json, using the public half you received out of band, not one from the bundle. - Load and start:
The script copies the bundle into a private (0700) directory and from then on uses only that copy. It runsTERRAFLOW_TRUSTED_KEYS=keys/trusted-keys.json deploy/offline/load-bundle.sh /media/offline-bundleterraflow offline verify(checksums, the manifest's signature by your pinned key, the image tar's own index, the Compose file), refuses any image the signed manifest does not list beforedocker load, checks every loaded image against the manifest, then starts the stack withdocker compose --profile app up -d. SetTERRAFLOW_OFFLINE_NO_START=1to stop before starting. - Create the plane signing key once (it refuses to overwrite an existing key):
docker compose exec dataplane terraflow keys init - Import the release files you were given:
terraflow release import <file>. A release is only ever staged; an approver activates it (see upgrades). - Check it is up:
curl -fsS http://127.0.0.1:8080/ready, then openhttps://<your-terraflow-host>/console/.
On every start the container applies database migrations (terraflow db upgrade) and the seed
profile (terraflow seed), then serves. The seed only fills a new installation: on one that has
activated rules it never creates, approves or activates anything, and everything it writes is in the audit
ledger.
Evaluation install (with network)
For a first look on a connected machine, deploy/compose/docker-compose.yml builds the image from the
repository and runs PostgreSQL, both Redis instances and the data plane, all on loopback ports:
cp .env.example .env && set -a && . ./.env && set +a
docker compose -f deploy/compose/docker-compose.yml --profile app up -d --build
# console: http://127.0.0.1:8080/console/
This file runs with TERRAFLOW_ENV=development and the demo estate. Use it to evaluate, not for real
data.
Connecting models and keys
Each model is a card in the model catalogue: where it runs (hosting, region,
jurisdiction), the most sensitive data class it may receive, the sensitive types it may receive,
its context window and its price. Adapters exist for openai_compat, anthropic,
azure_openai and bedrock.
Keys are never written into TerraFlow's files or database. A card holds a
reference: env:NAME (an environment variable) or file:/absolute/path (a file).
Anything else is rejected when the catalogue is loaded. Have your vault (for example a vault agent or your
platform's secret mount) deliver the secret as that variable or file.
- id: custcloud-in-standard
display_name: Azure OpenAI deployment, customer cloud (India)
adapter: azure_openai
endpoint_ref: env:AZURE_OPENAI_ENDPOINT # a reference, not the endpoint
credential_ref: env:AZURE_OPENAI_API_KEY # a reference, not the key
model: <your-deployment-name>
hosting: customer_cloud
region: in-central
jurisdiction: IN
max_data_class: confidential
allowed_sensitive_types: [pii]
context_window: 128000
Shape from seed/capabilities.yaml; validated by
terraflow/core/schemas/capability.py.
First rules
Rules are a contract per workload (one AI use case). A contract states:
- Data: permitted data classes and sensitive types, allowed jurisdictions, whether data may leave and to which named models.
- Models: the allowed models and model families, the baseline model savings are measured against, and a minimum quality bar.
- Money: per-request limit, hourly and monthly budgets, a spend-velocity limit.
- Purpose: audience, consequence tier and whether answers need human review.
Write or edit a contract on the console's Workloads screen. A different person must approve it before it runs. Approved contracts, the policy and the model catalogue reach the engine only as a bundle signed with the plane's key; an invalid or expired bundle fails closed. Check the active bundle with:
docker compose exec dataplane terraflow bundle verify
# bundle <version> OK — signer <key id>, expires <date>, <n> contracts, <n> capabilities
To see why a request was allowed or refused, open it in the console's Routing screen, or use
terraflow policy explain.
Verifying the audit chain
Every outcome, answered, cached, refused or failed, is appended to a hash-chained ledger, sealed in signed checkpoints by the plane key. It holds ids, reason codes, costs and timings, never prompt or answer text. Verify it from the command line, or from the console's Evidence screen:
docker compose exec dataplane terraflow ledger verify
# ledger OK — <n> events, <n> signed checkpoints
A failure names the first bad sequence number and the reason:
| Reason | Meaning |
|---|---|
UNSIGNED_TAIL | Events that no signed checkpoint covers |
CHECKPOINT_MISSING | A signed checkpoint was deleted |
LEDGER_TRUNCATED | The ledger is shorter than the local anchor records |
If the ledger no longer holds its anchor, the plane refuses every request with
EVIDENCE_UNAVAILABLE before anything is sent to a model. If you deliberately reset the database,
also remove <TERRAFLOW_KEYS_DIR>/ledger-anchor.
You can also fetch the audit events of one request through the API:
GET /v1/requests/{id}/evidence.
Upgrades
- Software. Load the new signed bundle with
deploy/offline/load-bundle.shexactly as for the first install. The new container applies database migrations on start. - Model catalogue and prices. Vendor releases are signed by the release key you pinned. A release is only ever staged; a customer approver activates it on the console's Deployment screen, which signs a new bundle with your plane's key. A release can re-price or retire a model and offer a new one, added switched off; it never opens a route on its own.
- Connected mode (optional). A plane can name a management plane in
plane.yamland sync with it every 30 seconds, sending only aggregates per workload and hour (counts, cost, health), or nothing, never content. Losing the link never stops governed requests. - Backups. Back up the PostgreSQL database and the keys directory together. The keys directory is what the audit chain is verified against.
Sources: deploy/offline/, deploy/compose/docker-compose.yml,
Dockerfile, docs/runbooks/offline-install.md, terraflow/cli.py,
terraflow/config/settings.py.