Configuration Reference

How you configure Terrantula depends on which side you're on:

Resolution order is consistent across the CLI: flag → environment variable → config file → built-in default. A --flag always wins; the config file is the persistent fallback you set once with terrantula login and terrantula use.

# All three configure the same thing; the flag wins this invocation.
terrantula --base-url https://api.example.com --token terr_xxx entities list
TERRANTULA_BASE_URL=https://api.example.com TERRANTULA_TOKEN=terr_xxx terrantula entities list
terrantula login --base-url https://api.example.com --token terr_xxx   # persists to the config file

CLI config file

The CLI persists your current selections to ~/.config/terrantula/config.json, created with 0600 permissions (owner read/write only). terrantula use <project-id> and the --org selection write the current context; terrantula login writes the base URL.

The API token is not in this file. It goes into the OS keyring — see Credential storage — and the file records only where it went, so the CLI can tell "the keyring is unreachable from this session" apart from "you never logged in".

{
  "baseUrl": "https://api.example.com",
  "currentOrg": "acme",
  "currentProject": "cattle-saas",
  "tokenInKeyring": true
}
KeyTypeDefaultDescription
baseUrlstring—API base URL the CLI/SDK connects to. Set by login --base-url.
tokenInKeyringboolean—Whether the API token is in the OS keyring. Not a credential. Set by login.
tokenstring—Bearer API token (terr_…), present only on a machine with no keyring, or under TERRANTULA_KEYRING=off. Plaintext at 0600 — see Credential storage.
localSecretsKeyInKeyringboolean—Whether the local-mode encryption key is in the OS keyring. Not a credential.
currentOrgstring—Default org slug for org/project-scoped commands. Set by --org / overridden per-invocation.
currentProjectstring—Default project for project-scoped commands. Set by terrantula use <project-id>.
NOTE

Local-first mode (terrantula dashboard) doesn't use baseUrl / token. It runs an embedded server backed by ~/.config/terrantula/local.db (SQLite) so the first-five-minutes onboarding works with zero pre-existing infrastructure. See the CLI Reference.


Global flags

These flags are accepted by every command and override the config file for that invocation.

FlagOverrides config keyDescription
--base-url <url>baseUrlAPI base URL.
--token <token>tokenAPI token (terr_…).
--org <id>currentOrgOrg slug for org/project-scoped commands.
--project <id>currentProjectProject ID for project-scoped commands.
--json—Output raw JSON instead of pretty JSON. Use in scripts/CI.
--file [path]—Path to a JSON/YAML file providing complex or bulk fields. Explicit flags override file values. Accepts ----separated multi-document blueprints.
TIP

For scripted/CI callers, prefer the environment variables over the config file — they let a Makefile or pipeline drive the CLI without a login step and without touching ~/.config.


CLI environment variables

Read by the CLI and SDK. Each maps to a flag or config key in the resolution chain above.

VariableMaps toDefaultDescription
TERRANTULA_BASE_URL--base-url / baseUrl—API base URL. Required (via flag, env, or config) for any non-local command.
TERRANTULA_TOKEN--token / token—Bearer API token (terr_…).
TERRANTULA_ORG_ID--org / currentOrg—Org slug for org/project-scoped commands.
TERRANTULA_PROJECT_ID--project / currentProjectterrantula (import commands only)Project ID for project-scoped commands.
TERRANTULA_DEBUG—unsetWhen set (any value), prints extra error detail (stack traces) on failure.
NO_COLOR—unsetWhen set, disables ANSI color in CLI output (honors the NO_COLOR convention).
TERRANTULA_TFC_ALLOW_CUSTOM_HOST—unsetSet to 1 to allow import-terraform to read state from a non-app.terraform.io TFC/TFE host. A safety gate — off by default.
TERRANTULA_TFC_SUPPRESS_HOST_WARNING—unsetSet to 1 to silence the custom-host warning emitted by the TFC reader.
# Drive the CLI from CI with no login step:
export TERRANTULA_BASE_URL=https://api.example.com
export TERRANTULA_TOKEN=terr_xxxxxxxxxxxxxxxx
export TERRANTULA_ORG_ID=acme
export TERRANTULA_PROJECT_ID=cattle-saas
terrantula apply --file blueprint.yaml --json

Project API tokens

Tokens are how the CLI, SDK, and any integration authenticate. They are terr_-prefixed bearer tokens sent as Authorization: Bearer terr_…. Tokens are org-owned API keys scoped to one project; mint and revoke them under /orgs/:orgId/projects/:projectId/tokens (requires the tokens:manage permission — admin/owner).

# The raw token value is returned EXACTLY ONCE, at creation.
curl -X POST https://api.example.com/orgs/acme/projects/cattle-saas/tokens \
  -H "Authorization: Bearer terr_existing" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-deploy","role":"write"}'
FieldTypeDefaultDescription
namestring—Human label for the token. Required.
roleowner | admin | write | readwriteThe role the token authenticates as. Determines which permissions it has — see RBAC & Drift.
cellScopesstring[]— (all cells)Restricts the token to entities/relationships/action-runs in the listed cells. Omit for all-cells access.
expiresAtISO 8601 datetime— (no expiry)When the token stops working. Omit for a non-expiring token.
WARNING

The raw token value is returned once, at creation, and never again. Only the terr_prefix is stored (the value is SHA-256 hashed at rest), so a leaked token cannot be recovered from the database — you can only revoke and re-mint. Store the value in your secret manager immediately.

INFO

A token scoped to one org cannot authenticate against the same project name in a different org — token auth is scoped by both org and project. This is why --org (or TERRANTULA_ORG_ID) is required for project-scoped commands.


Server environment variables

Set these on the API server (and worker), not on the CLI. They are required to self-host. Never commit their values; mount them as secrets.

Required

VariableDescription
DATABASE_URLThe privileged Postgres connection string, e.g. postgresql://user:pass@host:5432/terrantula. This role owns the tables, runs the migrations and owns the job queue's own schema. No request-serving code path uses it.
DATABASE_POOL_URLThe runtime pool connection string — a different role that owns nothing and holds no table grant, made a member of terrantula_pool WITH INHERIT FALSE. Every request reaches the database through this one, as does the schema-drift probe on /__deployment — it reads only pg_catalog, which is readable by every role, so it needs no ownership. See Two database roles below; the API refuses to serve if this role can reach a tenant table without entering a scope.
ENCRYPTION_KEY32-byte hex key used to encrypt third-party credentials at rest (OIDC client secrets, etc.). Generate with openssl rand -hex 32.
BETTER_AUTH_SECRETSecret used by better-auth for session signing and for HS256-signing the GitHub App install-state JWTs. Must be ≥ 32 bytes.
API_BASE_URLThe public URL the API is reachable at. Required because action webhooks and repository_dispatch workflows call back to ${API_BASE_URL}/…/action-runs/<run>/callback. Unset blocks post-merge dispatch with a clear error rather than parking runs forever.

Optional / deployment

VariableDefaultDescription
TERRANTULA_DEPLOYMENTcloudoss or cloud. cloud additionally serves the GitHub App and (future) billing routes; under oss those paths return 404. SSO is not gated by this variable — it's mounted in both modes. Self-hosters who don't need hosted integrations set oss. See the Single sign-on guide.
PORT3000Port the server listens on.
QUEUE_DRIVERpgJob-queue backend. pg (Postgres-backed) for self-host; sqs is for AWS Lambda deployments only.
BETTER_AUTH_URL—Public base URL of the auth handler (the API origin). Used for OAuth/SSO callbacks, and as the last-resort fallback for user-facing links when neither TERRANTULA_UI_BASE_URL nor TRUSTED_ORIGINS is set.
TRUSTED_ORIGINSemptyComma-separated list of origins allowed for browser auth (CORS/CSRF).
TERRANTULA_UI_BASE_URLfirst TRUSTED_ORIGINS entry, then BETTER_AUTH_URLPublic base URL of the UI. Used to build every user-facing link — invite acceptance, email verification, GitHub install callback. Set this on any split-origin deployment, or invite links will point at the API host.

GitHub App (cloud deployments)

Required only when TERRANTULA_DEPLOYMENT=cloud and you connect the GitHub App. See Triggers, GitHub App, Actions & Webhooks.

VariableDescription
GITHUB_APP_IDThe GitHub App's numeric ID.
GITHUB_APP_NAMEThe GitHub App's slug/name.
GITHUB_APP_PRIVATE_KEYThe App's PEM private key (PKCS#8). Mount as a multiline secret; never commit.
GITHUB_APP_WEBHOOK_SECRETHMAC secret used to verify inbound GitHub webhooks.

Two database roles

Terrantula bounds tenants with PostgreSQL row-level security, and the policies are only a boundary against a role that does not own the tables. Against an owning role neither of the two levers holds — measured on PostgreSQL 16:

  • A revoked privilege is not revoked. REVOKE ALL ON t FROM app makes the next read fail with permission denied, and then GRANT SELECT ON t TO app, run by that same role, succeeds. It re-grants itself at will.
  • FORCE ROW LEVEL SECURITY is not forced. With FORCE on and no policy naming it, the owner reads zero rows — and ALTER TABLE t NO FORCE ROW LEVEL SECURITY, run by that same role, succeeds. The cage has an inside handle.

Worse, the failure is quiet. A table's owner matches none of the policies, so outside a scope a SELECT returns zero rows, an UPDATE and a DELETE report success having changed nothing, and only an INSERT raises.

So the deployment runs two roles against one database:

RoleNamed byOwns tablesTable grantsUsed for
privilegedDATABASE_URLyesall (by ownership)migrations, the job queue's schema
poolDATABASE_POOL_URLnononeevery request, and the schema-drift probe

The pool role's only power is membership in terrantula_pool, the group role migration 0136 creates, and it must hold it WITH INHERIT FALSE and without ADMIN OPTION. Both matter and both fail silently when wrong: with INHERIT TRUE the role holds the tenant role's privileges ambiently with no scope entered, and a read that should be refused returns zero rows instead; with ADMIN OPTION it can grant itself INHERIT TRUE and get there on its own.

Creating the pool role is one out-of-band act, because a migration that created a login role with a password would ship that credential to every deployment that ran it. Either:

-- (a) make the group role itself login-capable
ALTER ROLE terrantula_pool LOGIN PASSWORD '<secret>';

-- (b) or point your own managed login role at it — what CNPG and
--     Crunchy deployments want, since they own the login role
CREATE ROLE app_pool LOGIN PASSWORD '<secret>';
GRANT terrantula_pool TO app_pool WITH INHERIT FALSE;

SET ROLE is transitive through an INHERIT FALSE membership chain, so (b) works exactly as (a) does while inheriting nothing at either hop.

DANGER

DATABASE_URL, DATABASE_POOL_URL, ENCRYPTION_KEY, BETTER_AUTH_SECRET, and GITHUB_APP_PRIVATE_KEY are secrets. Mount them from your secret manager; never commit them, log them, or print their values. Rotating ENCRYPTION_KEYrequires re-encrypting every stored credential — treat it as long-lived.


Bootstrap mode

On a fresh database with no tokens, the API runs in bootstrap mode: all requests are accepted without authentication so you can mint your first token. Creating any project token (POST /orgs/:orgId/projects/:projectId/tokens) closes bootstrap mode and turns on token enforcement for every subsequent request.

# Fresh DB: mint the first token while bootstrap mode is open.
curl -X POST https://api.example.com/orgs/terrantula/projects/terrantula/tokens \
  -H "Content-Type: application/json" \
  -d '{"name":"bootstrap","role":"owner"}'
WARNING

Bootstrap mode is open access. Mint your first token immediately on a fresh deployment, and never expose a tokenless server on a public network.


Credential storage

The CLI holds two secrets: your API token, and the encryption-at-rest key for local-mode secret values. Both live in the OS keyring — Keychain on macOS, libsecret on Linux, Credential Manager on Windows — under the service name terrantula.

TERRANTULA_KEYRING decides what happens on a machine that has no keyring, which is the normal state for a container, a CI runner, and an SSH session:

ValueBehaviour
auto (default)Use the keyring when one is available. Otherwise write to a 0600 file and say so on stdout.
requireKeyring or nothing. A missing keyring is an error; no credential is ever written to disk.
offNever touch the keyring. Use the 0600 file.

Setting TERRANTULA_CONFIG_DIR implies off unless TERRANTULA_KEYRING is set explicitly — a keyring entry does not live inside a directory, so a redirected config dir cannot isolate one.

  • Upgrading. A token already sitting in config.json is moved into the keyring the first time the CLI can reach one, and removed from the file only after it has been read back out. You are not logged out, and there is no moment where the only copy is in flight.
  • Headless sessions. A keyring-held token cannot be read from a session with no keyring. This is reported as exactly that, not as "not logged in". Pass TERRANTULA_TOKEN (from your secret manager) or --token there — which is what CI should do regardless, rather than running login at all.
  • Local-mode encryption key. Stored in the keyring for new installs. An existing ~/.config/terrantula/local-secrets.key is left exactly where it is and never migrated: it is the only thing that decrypts the values already in local.db, so moving it would trade a recoverable inconvenience for unrecoverable loss. When the key is in the keyring and the keyring cannot be reached, terrantula dashboard refuses to start rather than generating a replacement that would silently orphan every stored value.
  • Server credentials at rest. Third-party credentials (OIDC client secrets, catalog Secrets) are encrypted with ENCRYPTION_KEY. terr_ tokens are SHA-256 hashed — only the prefix is stored.
  • In logs. Terrantula logs only token prefixes (terr_xxxxxxxx…), never full values.