Skip to main content

Tenancy Model — Clients, Environments & Infra

Console models a customer's world as a three-level hierarchy. Getting the levels right is what lets one customer run several isolated infrastructures while still being billed, authorized, and notified as a single tenant.

The three levels​

LevelTableBoundary forKey columns
ClientclientsTenancy — one customer: billing, access root, notify-routing root, one CDM customerid, name, slug
EnvironmentenvironmentsInfrastructure isolation — a distinct infra estate under the customerclient_id, name, slug, UNIQUE(client_id, slug)
Infrahosts, clusters, k8s_nodes, k8s_namespaces, k8s_workloads, credentials, install_tokens, webhook_sources, pull_sources, …the actual machines / clusters / secretsvaries — see below

Infra tables do not share one tenancy column. Which column a table carries decides how far a grant reaches, so it is worth being exact:

Tableclient_idenvironment_idEffective scope
hosts—NOT NULLEnvironment-only
clustersNOT NULLNOT NULLBoth, always set
credentialsNOT NULLnullableClient, optionally narrowed to an environment
install_tokensNOT NULLnullableClient, optionally narrowed to an environment
webhook_sourcesNOT NULLnullableClient, optionally narrowed to an environment
pull_sourcesNOT NULLnullableClient, optionally narrowed to an environment

Two consequences that a "everything is per-environment" reading would get backwards:

  • A credential, install token, webhook source or pull source with a NULL environment_id is client-wide. It is visible to, and usable across, every environment under that client. Narrowing one to a single environment is opt-in, not the default.
  • assets carries neither column. It is a generic identity layer keyed by (asset_type, ref_id) pointing at the real row (hosts.id, clusters.id, …); tenancy is resolved by following that reference, never by reading a column on assets itself.

There is no k8s_inventory table — the Kubernetes inventory lives in clusters, k8s_nodes, k8s_namespaces and k8s_workloads.

Client vs. environment — which do I create?​

Rule of thumb

A new client is a new tenant. A new environment is new infra under the same tenant.

  • Same customer, different infrastructures → one client, multiple environments. The common case for a customer running separable estates (a core product + a payments product + an FP project).
  • Genuinely different customers (separate billing / access / legal entity) → separate clients.

Do not create a second client just to separate infra — you would fragment the customer's billing, RBAC root, and notification routing, and lose the single-tenant view. The environment layer exists precisely so you don't have to.

Worked example — Global Solutions / Global Pay​

Global Solutions is one customer (CDM CDM-105) running three separable estates. It is one Console client with three environments:

client: Global Solutions   (slug globalsolutions, jira_customer_key CDM-105)
├── environment: global-solutions → GS core infra
├── environment: global-pay → Global Pay infra (isolated)
└── environment: global-solutions-fp → FP infra

Global Pay's hosts, clusters, credentials and agents live under the global-pay environment and are fully isolated from GS core — yet Global Solutions stays one tenant for billing, access, and notifications. Spinning up a separate "Global Pay" client would have been the wrong move.

Two orthogonal axes — don't conflate them​

A client has two independent sub-structures. Both hang off the client; they are not the same thing:

AxisSub-entityIsolatesKeyed by
Infra / monitoringenvironmenthosts, clusters, credentials, agents, alertsenvironments.id → environment_id on infra
Service desk / notifyorg (JSM organization)Jira ticket → Telegram routingservice_desk_org_mappings.jira_organization_id

So "Global Pay" is simultaneously:

  • a service-desk org (jira_organization_id = 1213) for ticket-notification routing — see Routing Model, and
  • (should be) an environment for infra isolation.

The two are configured independently: a service_desk_org_mappings row does not create an environment, and creating an environment does not route tickets.

Creating & scoping environments​

  • Create: POST /api/v1/clients/{client_id}/environments with {name, slug} (unique per client), or the Console UI → client → Environments → Add.
  • Enroll infra: each environment issues its own install token; an agent enrolls into exactly one environment, which pins its hosts/inventory to that estate.
  • Scope access: RBAC grants can be environment-scoped — user_client_environments, team_client_environments, and a service account's environment_scope. Grant an engineer or team access to only the global-pay environment without exposing the rest of Global Solutions.

See Tenant Isolation & Authorization for how these scopes are enforced on every endpoint.