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.
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".
| Key | Type | Default | Description |
|---|---|---|---|
baseUrl | string | — | API base URL the CLI/SDK connects to. Set by login --base-url. |
tokenInKeyring | boolean | — | Whether the API token is in the OS keyring. Not a credential. Set by login. |
token | string | — | Bearer API token (terr_…), present only on a machine with no keyring, or under TERRANTULA_KEYRING=off. Plaintext at 0600 — see Credential storage. |
localSecretsKeyInKeyring | boolean | — | Whether the local-mode encryption key is in the OS keyring. Not a credential. |
currentOrg | string | — | Default org slug for org/project-scoped commands. Set by --org / overridden per-invocation. |
currentProject | string | — | Default project for project-scoped commands. Set by terrantula use <project-id>. |
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.
These flags are accepted by every command and override the config file for that invocation.
| Flag | Overrides config key | Description |
|---|---|---|
--base-url <url> | baseUrl | API base URL. |
--token <token> | token | API token (terr_…). |
--org <id> | currentOrg | Org slug for org/project-scoped commands. |
--project <id> | currentProject | Project 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. |
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.
Read by the CLI and SDK. Each maps to a flag or config key in the resolution chain above.
| Variable | Maps to | Default | Description |
|---|---|---|---|
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 / currentProject | terrantula (import commands only) | Project ID for project-scoped commands. |
TERRANTULA_DEBUG | — | unset | When set (any value), prints extra error detail (stack traces) on failure. |
NO_COLOR | — | unset | When set, disables ANSI color in CLI output (honors the NO_COLOR convention). |
TERRANTULA_TFC_ALLOW_CUSTOM_HOST | — | unset | Set 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 | — | unset | Set to 1 to silence the custom-host warning emitted by the TFC reader. |
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).
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Human label for the token. Required. |
role | owner | admin | write | read | write | The role the token authenticates as. Determines which permissions it has — see RBAC & Drift. |
cellScopes | string[] | — (all cells) | Restricts the token to entities/relationships/action-runs in the listed cells. Omit for all-cells access. |
expiresAt | ISO 8601 datetime | — (no expiry) | When the token stops working. Omit for a non-expiring token. |
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.
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.
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.
| Variable | Description |
|---|---|
DATABASE_URL | The 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_URL | The 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_KEY | 32-byte hex key used to encrypt third-party credentials at rest (OIDC client secrets, etc.). Generate with openssl rand -hex 32. |
BETTER_AUTH_SECRET | Secret used by better-auth for session signing and for HS256-signing the GitHub App install-state JWTs. Must be ≥ 32 bytes. |
API_BASE_URL | The 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. |
| Variable | Default | Description |
|---|---|---|
TERRANTULA_DEPLOYMENT | cloud | oss 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. |
PORT | 3000 | Port the server listens on. |
QUEUE_DRIVER | pg | Job-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_ORIGINS | empty | Comma-separated list of origins allowed for browser auth (CORS/CSRF). |
TERRANTULA_UI_BASE_URL | first TRUSTED_ORIGINS entry, then BETTER_AUTH_URL | Public 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. |
Required only when TERRANTULA_DEPLOYMENT=cloud and you connect the GitHub App.
See Triggers, GitHub App, Actions & Webhooks.
| Variable | Description |
|---|---|
GITHUB_APP_ID | The GitHub App's numeric ID. |
GITHUB_APP_NAME | The GitHub App's slug/name. |
GITHUB_APP_PRIVATE_KEY | The App's PEM private key (PKCS#8). Mount as a multiline secret; never commit. |
GITHUB_APP_WEBHOOK_SECRET | HMAC secret used to verify inbound GitHub webhooks. |
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:
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:
| Role | Named by | Owns tables | Table grants | Used for |
|---|---|---|---|---|
| privileged | DATABASE_URL | yes | all (by ownership) | migrations, the job queue's schema |
| pool | DATABASE_POOL_URL | no | none | every 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:
SET ROLE is transitive through an INHERIT FALSE membership chain, so (b)
works exactly as (a) does while inheriting nothing at either hop.
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.
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.
Bootstrap mode is open access. Mint your first token immediately on a fresh deployment, and never expose a tokenless server on a public network.
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:
| Value | Behaviour |
|---|---|
auto (default) | Use the keyring when one is available. Otherwise write to a 0600 file and say so on stdout. |
require | Keyring or nothing. A missing keyring is an error; no credential is ever written to disk. |
off | Never 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.
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.TERRANTULA_TOKEN (from your secret manager) or --token there — which is
what CI should do regardless, rather than running login at all.~/.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.ENCRYPTION_KEY. terr_ tokens are SHA-256 hashed — only the prefix is stored.terr_xxxxxxxx…), never full
values.login, use, and every command's flags.