Multi-tenancy & self-hosting

There are two senses of "tenant" in play here, and conflating them causes real confusion — so name them up front:

  • Your customers' tenants — the cattle. The Tenant entities you place on clusters. This is what the rest of Best Practices is about.
  • Terrantula's own tenancy — the orgs, projects, and teams inside Terrantula that decide who can see and change which fleet. That's the first half of this page.

The second half is self-hosting: running the whole cattle wedge on your own infrastructure with no SaaS dependency — the hard constraint the OSS-first persona (Sam) lives by.

Drawing Terrantula's tenancy boundaries

Terrantula organizes work as projects inside organizations, with role-based access control over each project's catalog and data.

Do put each independent fleet — each blast-radius boundary — in its own project. A project is the unit that holds one catalog (entity types, cells, relationships, Actions, secrets) and its entities.

Don't cram unrelated fleets into one project to save setup.

Because a project is the access and isolation boundary. RBAC, API tokens, and secrets are scoped to it. Two fleets that shouldn't share credentials, placement pools, or audit trails should not share a project. The right grain is usually one project per team-owned fleet, or per strongly-isolated environment.

Separate environments cleanly

Do keep prod and non-prod fleets isolated, and use envOverrides on Actions to point one definition at different repos, workspaces, or runner endpoints per environment.

Don't mix dev and prod entities in one cell, and don't reuse a prod secret in a dev Action.

Because environment isolation is the same discipline you already apply to Terraform state. A cell is a placement candidate set — mixing environments invites placing a prod tenant on a dev cluster. envOverrides lets you keep one Action definition (so the placement and lifecycle logic doesn't fork) while the trigger targets the right environment at dispatch. See the Environments how-to for the end-to-end shape.

Scope access with RBAC and least privilege

Do grant the narrowest role that lets someone do their job, and split read from write:

  • Read-only access (the read-only graph projection) for people who need visibility — Riley's on-ramp use case.
  • Write access to catalog/data for the people who model and operate the fleet.
  • The narrowest possible set for anything touching secrets or apply.

Don't hand everyone the highest role because it's less friction.

Because Terrantula's permission model splits along resource verbs (catalog:read, data:write, secrets:set-value, apply:write, …) precisely so you can grant visibility without granting the ability to fire Actions. The full role-and-permission vocabulary is in the RBAC reference. Visibility-only access is a legitimate, common posture — many teams start there.

Issue scoped API tokens for automation

Do mint a dedicated, narrowly-scoped project API token for each CI pipeline or script (terr_…), and rotate them.

Don't reuse a human's session token in automation, or share one token across many pipelines.

Because terr_-prefixed tokens are hashed before storage and scoped to a project's RBAC — a token is the audit identity for everything it does. A per-pipeline token means a leaked or rotated credential has a contained blast radius and a clear owner. The CLI reference covers token use in CI.

Keep secrets out of the catalog

Do declare secrets by name in the catalog and set their values out-of-band with terrantula secrets set-value.

Don't commit a credential into catalog YAML, even in a private repo.

Because secret values are encrypted at rest with the server's ENCRYPTION_KEY and never appear in your catalog, version control, or logs. A per-environment, per-project secret of the same name (github-token, tfc-api-token) keeps prod and dev credentials genuinely separate while your Action YAML stays identical.


Self-hosting the cattle wedge

The Terrantula backend is Apache 2.0 and self-hostable end-to-end. The entire cattle wedge — multi-tenancy, governance, Actions, the cascade — runs with zero SaaS dependencies. This is a binding product commitment, not a tier: no cattle-wedge feature is gated behind a paywall.

Sam's constraint is the design constraint

The OSS-first test is simple: could you run the whole wedge with no paid subscription?The answer must always be yes. Terrantula OSS + your Terraform substrate + your own CI = full functionality at $0 SaaS spend. If a workflow seems to require subscribing to something, you've taken a wrong turn — re-check it against this constraint.

Bring your own Postgres; own the layer above Terraform

Do run the API and worker against your own Postgres (14+), set DATABASE_URL and ENCRYPTION_KEY, and deploy with the Kubernetes/Helm guide.

Don't expect Terrantula to host your state, your runners, or your Postgres.

Because Terrantula owns exactly one layer — the fleet model above your Terraform — and nothing below it. You keep your state backend, your runner, your CI. The queue is Postgres-native (pg-boss), so a working deployment is just the API, the worker, and a database — no extra infrastructure to operate. Full configuration is in the install guide.

Sit on top of the runner you already have — never replace it

Do point Actions at the substrate you already run: the pull-request trigger for bare Terraform + GitHub Actions, terraform-cloud for a TFC/HCP workspace, atlantis for your Atlantis, atmos-workflow for Atmos.

Don't look for a hosted Terrantula runner pool — there isn't one, by design.

Because Terrantula sits on top of runners and never replaces them. For substrates without a hosted API — Atmos and Atlantis — Terrantula ships reference runner implementations you deploy yourself (a customer-deployed wrapper, not a Terrantula-hosted service). The Atmos workflow trigger calls your reference runner endpoint; nothing routes through a hosted service unless you deliberately choose the hosted option. Substrate priority is Terraform first, Atmos as a peer, OpenTofu second.

Self-hosting doesn't change the model — only where it runs

Do use the same catalog YAML, the same Actions, and the same first-five-minutes onboarding (terrantula import-terraformterrantula dashboard) self-hosted as you would hosted.

Because self-hosting is a deployment choice, not a different product. The local-first path (an embedded SQLite DB, no signup) is for one person trying it out; a self-hosted server with Postgres is for a team. The catalog, the personas it serves, and every commitment on these pages are identical across all three. When you outgrow local mode, terrantula login points the same commands at your server.

The hosted offering adds operations, not capabilities

Do reach for the hosted option if you'd rather not operate Postgres yourself, or want managed multi-tenancy, SSO/OIDC, and audit out of the box.

Don't assume the hosted option unlocks fleet features the OSS path lacks — it doesn't.

Because the hosted offering adds operations, support, and the polished UI on top of the same Apache 2.0 backend; it doesn't fork the API or gate the wedge. The self-hosted path is never second-class.

Common mistakes (the anti-patterns)

  • One project for everything. Collapsing isolation boundaries means one leaked token or one bad Action reaches every fleet. One project per blast-radius boundary.
  • A shared admin role. Handing everyone write/apply access erases the read/write split that lets you offer safe visibility. Grant least privilege; default visibility-only.
  • Looking for a hosted runner. There isn't one — that's the commitment. Deploy a reference runner for Atmos/Atlantis; use the native API for TFC.
  • A secret in the catalog. Even in a private repo, a committed credential is a credential in git history. Declare by name, set out-of-band.
  • Assuming self-host is a downgrade. It runs the entire wedge at $0 SaaS spend. That's the design center, not a fallback.

Back to: Best Practices overview · Reference: RBAC roles & permissions · Triggers · Deploy on Kubernetes