Skip to main content

Architecture

Strataplane is a control plane for stock telemetry agents. It stores a layered, typed spec of what every agent should run, renders one complete config per agent instance, delivers it over the agent's own protocol, and tracks what each agent actually applied. It manages Telegraf, Elastic Agent and Vector, and runs synthetic journeys on strataplane-agent. An installation hosts one or more tenants: self-hosted for one organisation, or as a multi-tenant service (§12).

The documents are listed in the README.

1. Principles​

  • Stock agents. No Strataplane code is required on the managed host for the base functionality. Telegraf pulls --config <url> and reports through [[outputs.heartbeat]]. Elastic Agent enrolls against Strataplane as if it were Fleet Server. Vector polls its config with its http provider and reports through an http sink of its internal metrics and logs. The helper (strataplane-agent) is only needed for things stock agents cannot do: synthetic journeys.
  • Typed spec, not config blobs. Plugin instances are structured trees with a key, an owner and a provenance per value. Layers with label selectors replace copy-pasted per-host files. Raw TOML/JSON is an escape hatch, not the model.
  • One render graph per agent instance. Everything downstream (the delivered config, the secrets an agent may fetch, the impact set of a change, approvals, rollouts) is a projection of the effective rendered config. Nothing is configured twice.
  • Bash first. The spec is a tree of files. Every capability is a command: stratactl for humans and CI, the same commands inside the assistant's sandboxed shell. The UI is a view over the same API, not a separate model. See §8.
  • Writes are reviewed change sets. Spec changes are staged as an overlay on a base revision, validated, impact-assessed, approved and merged into a new immutable revision. Delivery to agents is a separate rollout step.
  • Desired vs applied, always. Every agent instance has a desired artifact and an applied artifact. The difference is the main health signal and the basis for rollout guardrails.
  • Postgres plus objects. One embedded Postgres for single-node installs, any external Postgres (≥ 15) for larger and HA installs, no Postgres extensions. Large binaries go to object storage: a local directory on a single node, an S3-compatible bucket otherwise (operations.md §9).
  • Air-gappable. No runtime downloads. Postgres binaries, Telegraf plugin and Vector component schemas, wasm modules and validator binaries arrive as signed bundles that operators import.

2. Components​

┌──────────────── strataplane (single Rust binary) ────────────────┐
Browser (Svelte) ──▶│ management API /api/v1 ──┐ │
├ UI │ static UI (embedded) │ ┌───────────┐ ┌───────────┐ │
├ Pi Durable chat │ assistant LLM proxy ─────┼──▶│ core │──▶│ renderer │ │
└ just-bash + wasm │ │ │ spec, │ │ + modules │ │
stratactl / CI ────▶│ │ │ inventory,│ └─────┬─────┘ │
│ agent API ───────────────┼──▶│ change │ ▼ │
Telegraf ──────────▶│ /agent/v1/telegraf/* │ │ sets, │ ┌───────────┐ │
Elastic Agent ─────▶│ /api/{status,fleet/*} │ │ rollouts │ │ artifacts │ │
strataplane-agent ─▶│ /agent/v1/helper/* │ └─────┬─────┘ └───────────┘ │
Vector ────────────▶│ /agent/v1/vector/* │ │ │
│ /metrics │ │ controllers (per shard): │
│ │ │ reconciler · rollout · │
│ │ │ status · placement · jobs │
└──────────────────────────┴─────────┼─────────────────────────────┘
▼
Postgres (embedded child process, or external / HA) + object storage

One binary, strataplane, contains:

PartResponsibility
HTTP serverTwo listeners: the management listener (UI, /api/v1, /metrics) and the agent listener (/agent/v1/*, Fleet emulation). The agent listener can require or request client certificates without affecting browser users. A single listener is allowed for small installs. Every request is resolved to its tenant first (§12).
CoreTenants, spec store (blobs, revisions, change sets), inventory (hosts, labels, agent instances), selector evaluation, merge engine with provenance, authorization, audit. Agent-kind agnostic.
Agent modulesOne per agent kind. Each implements frontend (wire protocol), render, validate, deliver and status ingest (§5): Telegraf, Elastic Agent, Vector and strataplane-agent (journeys).
ControllersBackground loops: reconciler (desired artifact per instance), rollout controller, status scheduler (staleness), placement controller (spec-format.md §3.9), audit export, blob sweeper, retention. Each tenant belongs to one controller shard; each shard runs on one replica at a time (§9).
Operation workersLong requests (change set validation, catalog import, tenant deletion) as durable tasks with checkpointed steps on absurd (operation); any replica claims them.
Assistant backendLLM proxy for the browser-side Pi Durable harness, chat commit log storage, server-side telegraf-parse and vector-vrl via wasmtime.
Embedded Postgres supervisorStarts, health-checks and stops a bundled Postgres when no DATABASE_URL is configured (Operations).

Separate binaries:

BinaryPurpose
stratactlCLI over the management API. Pulls the spec tree into a directory, runs render/explain/impact/validate against local edits, submits change sets, drives rollouts. Also runs telegraf-parse and vector-vrl locally on wasmtime.
strataplane-agentOptional host helper in Rust: the journey runner (journeys.md).

3. Domain model in one page​

Tables are in schema.md, generated from the migrations. The essentials:

Tenant (slug, display name, state, limits)
└─ everything below, per tenant

Host (stable id, name, kind, owner team, effective labels)
└─ AgentInstance (kind: telegraf | elastic-agent | vector | strataplane-agent, external id, version,
health, desired_artifact, applied_artifact, identity, assurance)

Spec revision rN (immutable tree of spec files, content-addressed)
├─ Layer (agent kind, precedence, selector, owner, instances{key → PluginInstance}, bundles[], routes[])
├─ Bundle (named, reusable set of instances of one agent kind)
├─ Route (named output, per agent kind)
├─ Journey (inline script, runner selector; journeys.md)
├─ Placement (pick N online hosts, label them)
├─ Settings (render levels, Telegraf and Vector heartbeat, Elastic poll timeout, journey metrics, retention, assistant)
└─ ApprovalPolicy, RolloutPolicy

ChangeSet (base revision + overlay of changed files) → validation, impact, approvals → merge → rN+1
Rollout (from rN to rM, stages by selector with guardrails) → moves the release pointer
Artifact (rendered config, content-addressed, never contains secret values)
Secret (key, owner, grant selector, min assurance) ── SecretVersion (write-only value)

Selectors, ACL scopes, approval rules and secret grants all evaluate against host labels. Tokens, versions and desired/applied state belong to the agent instance. A host running Telegraf and Elastic Agent has one host and two agent instances; the instances share labels and selectors but receive layers of their own kind and never merge into each other.

4. Render pipeline​

For one agent instance at spec revision R (a Telegraf class default renders the same way, with the class token's labels as host labels; agents/telegraf.md):

  1. Resolve labels. Effective host labels from all label sources, by source precedence (label_source). Agent facts (os, arch) are labels too.
  2. Select. Collect every layer at R of the instance's agent kind whose selector matches the host and whose agentVersions admit the instance's version. Expand included bundles and routes. Order by precedence (ties are a validation error).
  3. Merge. Deep-merge instances by key in precedence order with the per-path directives (append, replace, remove) and the instance ensure state (present, absent). Every leaf value records the layer that set it: this is the provenance behind explain (spec-format.md §4).
  4. Module render. The agent module turns the merged tree into its native format at the revision's render level and injects module-owned blocks (Telegraf heartbeat output, Vector heartbeat pipeline, Elastic Agent fleet.hosts). Output is deterministic: same input, same bytes.
  5. Hash. The artifact hash is SHA-256 over the rendered bytes. Artifacts are stored once per hash and hold secret references only (§11).

Rendering is a pure function of (revision, matching layers, agent kind, agent version, current versions of the referenced secrets, server.agent_url); spec values are literal, so labels matter only through selectors. Changing the installation's agent_url re-renders every agent at its desired revision, without a rollout. The catalog validates but never renders.

Render levels. The renderer's behaviour is part of the revision: the renderLevel of the agent kind's Settings object (journeys for strataplane-agent; spec-format.md §3.8). A Strataplane release that changes rendered output adds a level and keeps rendering every earlier level of its major version. A re-render for a secret, label or agent version change therefore produces exactly what the agent's revision produces, pinned or not, and new renderer behaviour reaches agents only through a change set that raises the level and the rollouts that deliver it.

Artifacts carry the secret versions they were rendered against (never values), so a new or disabled secret version changes the artifact of exactly the agents that reference the secret, and each module's ordinary delivery brings them the new value. The reconciler re-renders when a release, a pin, a label, an agent version or a referenced secret's current version changes. A secret version change re-renders at each agent's current desired revision, so it takes effect without a rollout. Unchanged hashes are no-ops, so a spec change only touches the agents whose artifact actually changes; they are the core of the change's impact set (spec-format.md §6). A Strataplane upgrade alone changes no rendered bytes (operations.md §7).

Because rendering is pure, agents with identical inputs form an equivalence class that renders once. Previews, impact and the reconciler work per class and expand to members only where needed (approval requirements, rollout stages). strataplane_render_classes shows how many classes exist; per-host node layers make their host a class of its own.

5. Agent module contract​

The core never handles agent-native TOML, policy JSON or wire protocols. Each module implements:

/// Sketch, not final API.
trait AgentModule: Send + Sync {
fn kind(&self) -> AgentKind; // "telegraf", "elastic-agent", "vector", "strataplane-agent"
fn routes(&self) -> axum::Router<AppState>; // wire protocol on the agent listener
fn schema(&self, version: &Version) -> Result<PluginCatalog>; // per agent version
fn render(&self, tree: &MergedTree, ctx: &RenderCtx) -> Result<Artifact>;
fn validate(&self, artifact: &Artifact, ctx: &ValidateCtx) -> Vec<Diagnostic>;
fn delivery(&self) -> DeliveryMode; // Pull (Telegraf, Vector) | Push (Elastic actions)
fn ingest_status(&self, raw: StatusReport) -> Result<StatusUpdate>;
fn capabilities(&self) -> &[Capability]; // settings, unenroll …
}
OperationTelegrafElastic AgentVectorstrataplane-agent (journeys)
Wire protocolGET/HEAD /agent/v1/telegraf/config, POST …/heartbeat, GET …/secrets, POST …/enrollGET /api/status, POST /api/fleet/agents/enroll, …/{id}/checkin (long-poll), …/{id}/acks, …/{id}/audit/unenrollPOST /agent/v1/vector/enroll, GET …/config (polled by the http provider), POST …/heartbeatPOST /agent/v1/helper/enroll, GET …/config, POST …/checkin (short poll), POST …/results
AuthPer-agent bearer token (TELEGRAF_CONTROLLER_TOKEN), optional mTLS on secrets and heartbeatEnrollment token → per-agent access API key (Authorization: ApiKey …), optional mTLSEnrollment token → per-agent bearer token (spv_…) inline in the bootstrap, optional mTLS on every requestEnrollment token → per-agent bearer token (sph_…), optional mTLS
RenderTOML, one table per instance, alias = instance name, secrets as @{strataplane:key}Agent policy (JSON): outputs, inputs[] with use_output, agent.*, fleet.hosts; secrets inlined at deliveryTOML, one table per component, id = instance name, inputs wildcards expanded; token, artifact hash and secrets filled in when servedJSON: the journeys whose selector matches the host; no secrets
ValidatePlugin schema of the agent's version, telegraf config check sandboxPolicy schema + component specs of the agent's version, optional elastic-agent sandboxComponent schema of the agent's version, vector validate --no-environment sandboxScript parses in QuickJS and calls only the journey API (L2); no sandbox
DeliverStateless pull. Last-Modified per instance changes when the desired artifact changes; Telegraf's URL watcher reloads.POLICY_CHANGE action queued per instance, returned on checkin, acknowledged via acksStateless pull: the provider fetches every pollInterval and reloadsThe checkin returns the desired artifact hash; the runner pulls when it differs from its applied one
Applied stateArtifact hash in a heartbeat header rendered into the config itselfpolicy_revision_idx in the checkin plus the action ackArtifact hash in a heartbeat header rendered into the config itselfArtifact hash in the checkin
HealthHeartbeat status, statistics, logsCheckin status, per component/unit stateHeartbeat batches: error and fatal logs, component error countersCheckin health, Chromium version, dropped results; journey results separately

Details: agents/telegraf.md, agents/elastic-agent.md, agents/vector.md.

6. Change, release and rollout flow​

edit (UI / stratactl / assistant)
│ overlay on base revision rN
▼
ChangeSet: draft ──validate, submit──▶ in_review ──approvals, merge──▶ rN+1
│ schema, sandbox, impact set, │
│ approval requirements ▼
Rollout rN → rN+1: stage 1 … stage k
guardrails per stage, halt on breach
│
▼
release pointer = rN+1
  • Revisions are per tenant and immutable. A revision is a content-addressed tree of spec files (like a Git tree). Merging a change set creates the next revision from the reviewed tree only: its base must be the latest revision. A moved base needs a rebase, which is validated and reviewed again (change_set_state).
  • History is native: log per file and object, diffs between any revisions, and revert or restore as ordinary change sets. Strataplane is the only source of truth for the spec.
  • The release pointer is the revision that agents outside any active rollout receive. Moving it is the job of a rollout. Small installs can use a rollout policy with a single "all at once" stage, which makes merge and release one click.
  • Rollouts assign the new revision to impacted agents stage by stage, each stage a selector, and halt when a guardrail is breached (spec-format.md §3.7; states and actions in rollout_state).
  • Pins override the release pointer per selector or agent: a pin keeps its agents on its revision, optionally for a one-off window, and out of rollout stages. Pins fix the revision, not the artifact, so secret rotations still reach pinned agents (§4).
  • One active rollout per agent kind. A rollout starts when it is created, at merge or by an operator. Change sets merged meanwhile wait; the next rollout takes the latest revision and so delivers them together.

Approvals are computed from the impact set (spec-format.md §6).

7. Authorization​

Permission decisions use embedded Cedar through sp-authz. The entity and binding projection, SQL filtering, error handling and their verification are specified in authorization.md. The permissions per API group are defined here.

  • Tenants first. Everything below holds inside one tenant. A user acts in a tenant only as its member; roles, teams, service accounts and tokens belong to one tenant. Platform admins manage tenants and the catalog and hold no role in a tenant unless one of its bindings grants it (§12).
  • Subjects: users (local or OIDC), teams (from OIDC groups claims or managed locally), service accounts (users of kind service that act through API tokens), and the assistant acting as the current user (no separate privileges).
  • Objects: spec files (by owner team and path), hosts and agents (by label selector, default owner = <team>), secrets (by owner team and grant), rollouts.
  • Roles: viewer, author (edit drafts, submit), approver, operator (rollouts, pins, agent actions), secret-admin (write secret values, grants), admin. admin is bound to the whole tenant; besides tenant administration it holds what author, approver and operator hold (it also satisfies the admin requirement of reviews), but not secret-admin. Roles are not ordered, but every role reads what it is scoped to, and a binding of any role and scope gives what the table below grants viewer on tenant-wide data (tenant, catalog, placements, audit of own actions). Role bindings are (subject, role, scope) where the scope is an owner team, a label selector, or the whole tenant; a token can be limited to a subset of its owner's roles.
  • Change rule: whoever submits or merges a change set (its author or another user or service account) must be allowed to edit every changed file and every host in the impact set; a changed Secret also needs secret-admin of its owner team, before and after the change. Merge rechecks the rule against current inventory and bindings; merging with start_rollout also needs what starting those rollouts needs.
  • Secrets are write-only. There is no read verb for secret values.

Per API group:

GroupReadWrite
Hosts, agentsviewer within scopeoperator (actions, quarantine, manual labels), admin (delete, host owner); exploration on a runner (journeys.md §8.2): author or operator within scope; pairing one's own computer as a runner: author or operator in any scope
Spec, previewviewerchange sets: author for files they own (edits and rebase); submit and merge follow the change rule
Reviewsviewerapprove and reject: approver of the required team, human users only (spec-format.md §6)
Rollouts, pinsvieweroperator within scope, over every host the rollout or pin can reach
Secretsmetadata: viewer; deliveries: secret-adminvalues: secret-admin of the owner team at the latest revision
Enrollment (tokens, approvals), trust anchors, network zones, providersadminadmin
Users, teams, role bindings, service accountsadminadmin
API tokensown tokens; admin allown tokens (roles at most the owner's); admin for service accounts
Tenant (/api/v1/tenant: limits, usage)viewer– (platform)
Catalogviewerplatform admin (import)
Platform (/api/v1/platform/*: tenants)platform adminplatform admin
Placementsviewer; picks within scope– (placements change through change sets)
Journeys, runsviewer; evidence: viewer scoped to the journey's owner team– (journeys change through change sets); a manual run: host.explore on each runner (journeys.md §3.1)
Assistantown chatsown chats
Auditadmin (all), others own actions–

The check each operation makes is in openapi.yaml (x-strataplane-authz).

Every mutating request writes an audit_event to its tenant's hash chain, the assistant's on behalf of its user (assistant.md §1); platform actions write platform_audit_event. Every secret delivery writes a secret_delivery row.

8. Bash-first command surface​

Strataplane follows the pattern proven in the Grafana Pi assistant: capabilities are shell commands over a file tree, not bespoke UI flows or agent tools.

  • The spec is files. stratactl spec pull writes the spec tree of a revision into a directory; editing is $EDITOR, sed, or the assistant's write/edit tools. The overlay of changed files is the change set.
  • Commands do the domain work. render, explain, impact, validate, diff, changeset submit, rollout show, telegraf-parse, vector-vrl (cli.md). They are thin clients of stateless preview endpoints (POST /api/v1/preview/*) that accept a base revision plus an overlay, so the CLI, the assistant and the UI get identical answers from one Rust implementation.
  • The assistant runs the same commands in a sandboxed shell over a per-chat session filesystem, as the current user (assistant.md).
  • One registry. Command names, arguments, --help text and declared effects (cli.md §2) are defined once and shared by stratactl and the assistant shell, so prompts, docs and CLI do not drift.

9. Persistence and HA​

Without DATABASE_URL the binary supervises a bundled Postgres; with it, any Postgres ≥ 15. In HA mode, N stateless replicas serve all traffic on one external Postgres and one bucket. Controllers run per shard: a replica runs the shards whose advisory lock it holds, so one tenant's re-render or rollout never stalls the controllers of tenants on other shards. Cross-replica wake-ups use LISTEN/NOTIFY. Correctness never depends on a notification: every waiter re-reads state on wake-up and on timeout. Heartbeats and checkins are coalesced in memory and flushed in batches; overload returns 503 with Retry-After. The database keeps current state, status transitions and bounded logs, not heartbeat history; time series go to the metrics endpoints (§10). Large binaries are objects (operations.md §9). Details in operations.md.

10. Observability of Strataplane itself​

  • /metrics (Prometheus, installation; platform admins, on the platform host): agents by tenant, kind, host kind, health and sync state; render and validation errors; heartbeat and checkin rates (Telegraf, Elastic Agent, Vector, strataplane-agent) and shed heartbeats; render classes; DB pool, job queue depth, controller shard lag, object store errors. Gauges are computed at each scrape, so a series that no longer applies disappears instead of keeping its last value. Each replica reports its own shards' lag and classes. Histograms, per replica: strataplane_http_request_duration_seconds (route template, method, status; both listeners, so agent check-ins, config fetches and secret fetches too), strataplane_controller_step_duration_seconds (rollout, placement, reconcile, status, tenant and installation jobs, tenant deletion; result), strataplane_render_duration_seconds (agent kind, result) and strataplane_operation_duration_seconds (operation kind, result); counters strataplane_rollout_steps_total (stage started, skipped, passed, halted, completed) and strataplane_secret_deliveries_total (agent kind, channel, delivered or denied).
  • GET /api/v1/metrics (Prometheus, per tenant, with a viewer token of the tenant): agents by kind, health, sync state and version; change sets by state; rollout stage state and guardrail values; secret deliveries; journey series (journeys.md §5).
  • Structured logs (tracing, JSON). Each request runs in a request span with its request id, method, route template, tenant, principal and status; controller steps and operations have their own spans. The request id is the client's X-Request-Id when valid, else a new UUID; it is returned in X-Request-Id, as request_id in problem documents and in the request's audit events.
  • Built-in alert rule templates for the platform itself (agent staleness except on clients, applied ≠ desired beyond grace, rollout halted, render and validation failures, controller lag) exported as Prometheus rule files.

11. Security boundaries​

  • Rendered artifacts contain secret references only. Telegraf fetches values over secretstores.http from /agent/v1/telegraf/secrets; Elastic Agent policies and Vector configs get values inlined at delivery, never stored server-side in plaintext.
  • Agent credentials (Telegraf and Vector bearer tokens, Elastic access API keys, enrollment tokens) are stored as SHA-256 hashes; tokens have a recognizable prefix for secret scanning: spk_ (API tokens), spn_ (enrollment), spt_ (Telegraf agents), spe_ (Elastic Agent access keys), spv_ (Vector agents), sph_ (strataplane-agent).
  • The full config URL is logged by Telegraf, so URLs never carry credentials.
  • Client certificates per tenant. Trust anchors belong to a tenant. The agent listener requests a client certificate and checks the key possession proof in the handshake against any chain; the chain is verified against the trust anchors of the tenant that the request's credential names (§12). Every agent request that can carry a certificate also carries a credential.
  • Egress. URLs a tenant configures (audit webhooks and syslog targets, LLM provider base URLs, trust anchor CRL URLs) are dialled only to addresses outside egress.deny (default: loopback, link-local and the installation's own networks; operations.md §4).
  • The assistant shell has no network, process or link commands. Parser wasm runs without I/O. The LLM API key never reaches the browser.
  • Secret values are encrypted under a data key per tenant, wrapped by a deployment key kept outside the database, so a dump alone exposes no value and deleting a tenant's data keys shreds its ciphertext (operations.md §6).
  • Assurance has two levels. A request is high when it carries the client certificate bound to the instance at enrollment, from a trust anchor; everything else, including approved token enrollments and Telegraf class mode, is low. An instance with a bound certificate must present it on every request that can carry one (all but Telegraf's config fetch), else the request is 401. Each such request checks that the leaf is the bound one, that every certificate in its chain is valid now and chains to the bound trust anchor, and that the anchor's CRL does not revoke it. Each replica fetches a CRL from crl_url on first use and then hourly; a failed fetch keeps the last CRL fetched, and before the first success no revocation is checked. Deleting the anchor unbinds the certificate, and the instance becomes low.
  • Delivery rule. An agent instance receives secret K only if its desired or applied artifact references K, the Secret exists at the latest revision and its grant selector there matches the host, the assurance of the delivering request is at least that Secret's minAssurance, and the instance is approved and not quarantined. The latest revision governs, so narrowing a grant takes effect at merge for every agent, pinned or not. The reconciler renders a secret reference only for an instance the rule can admit (grant matches, a bound certificate where high is required, not a class instance); otherwise the render fails (render_error), since Telegraf exits on a missing secret. Every delivery is audited (secret_delivery; Vector, which fetches its config every poll, when an instance is first served an artifact). Telegraf class instances receive no secrets: their configs are addressed by source IP and can briefly reach another client of the class (agents/telegraf.md).

12. Tenancy​

An installation hosts tenants. A tenant is the unit of isolation: its own spec and revisions, inventory, agents, enrollment tokens and trust anchors, secrets and data keys, change sets, rollouts, teams and role bindings, audit chain, chats and limits. Shared by all tenants: user identities, the catalog, and the server itself.

An installation runs in one of two modes:

Self-hostedService
Selected byno [billing] sectiona [billing] section (operations.md §4)
Tenantsexactly one, created by strataplane initmany, created by the billing service
Membershipscreated at the first OIDC login; local users added by a tenant adminmanaged by the billing service; a login only reads them
Teamslocal, or synced from the groups claimlocal
Platform APIplatform admins, also with a credential of the tenantplatform sessions and platform tokens

Finding the tenant. One host (server.public_url) serves every tenant. Every request is resolved to one tenant before anything else runs:

RequestTenant from
Management APIThe credential. An API token belongs to one tenant (a platform token to none). A session is bound to the tenant chosen at login; switching tenants opens a new session (POST /api/v1/auth/session). A session bound to nothing yet reaches only /api/v1/auth/* and /api/v1/me (403 auth.no_tenant elsewhere).
Agent listenerThe credential: enrollment token, class token, agent token or Fleet access key. All tenants share one agent host; credential hashes are unique across tenants. /api/status needs no tenant.
ControllersThe tenants of the shard, one tenant per transaction.

Isolation. Every tenant row carries its tenant_id, references between tenant rows are composite foreign keys, and row-level security hides other tenants' rows from every query that forgot its filter (schema.md, row-level security). Content-addressed data (spec blobs, artifacts) is stored per tenant even when bytes match, so no tenant can probe for another's content. Objects live under the tenant's key prefix, encrypted with its data key (operations.md §9). Controllers round-robin between the tenants of a shard, and render workers and operations (sandbox validation among them) have a concurrency limit per tenant (controllers.tenant_operations, default 2, across all replicas): an operation beyond it waits as queued until one of its tenant's finishes.

Identity. The installation trusts one OIDC issuer (auth.oidc_issuer) and identifies a user by (issuer, sub); no organization or tenant claim is read. The redirect URI is <server.public_url>/api/v1/auth/oidc/callback. In service mode, and self-hosted with auth.require_verified_email, an ID token without email_verified: true opens no session: the user is sent to a page asking them to confirm their address in the identity provider (the billing service can send the confirmation again) and to sign in again. Local users (auth.local_users) log in with a password; the local break-glass admin created by strataplane init is a platform admin and, self-hosted, an admin of the tenant. An unknown username takes as long to refuse as a wrong password. After 5 failed logins for a username, or 20 from a client address (an IPv6 /64), within a minute, the replica refuses further attempts with 429 auth.throttled until the minute ends; a successful login resets the username's count.

A login opens a session bound to:

  • self-hosted: the installation's tenant for its members (an OIDC login first makes the user a member and syncs teams from auth.groups_claim: a group maps to the team whose external_id is the group name, synced at every login), else the platform for a platform admin;
  • service mode: the user's only active tenant; otherwise nothing, and the UI offers the user's tenants (GET /api/v1/auth/tenants) and, for platform admins, a platform session, also through the tenant menu later. A user without a membership sees that they have no access.

New tenants get a team admins (external id auth.admin_group, default strataplane-admins) with an admin binding on the whole tenant; in a self-hosted installation the members of that group become its admins. A customer's own identity provider federates in the identity provider, never in Strataplane. Platform admins list tenants, change their limits, suspend, resume and delete them through /api/v1/platform/tenants; they create none. With auth.platform_admins set, every OIDC login sets the user's platform admin flag from that list of subjects.

Service mode. The billing service (billing.url) owns customer accounts, plans and payments; Strataplane implements its product contracts (memberships, entitlements, billing API v1 in the billing repository):

  • Pushes. Billing pushes to /internal/billing/memberships/{id} and /internal/billing/entitlements/{tenant} on the management listener (openapi.yaml, not routed publicly). Each push carries a Zitadel access token (JWT) of billing's provisioning machine user: verified against the issuer's JWKS, with billing.project_id in its audience and billing.provisioning_subject as subject. A push with an older version than the stored one changes nothing.
  • Tenants. The first push for an unknown workspace (external_id) creates its tenant, named by the workspace name; the slug is derived from it and fixed. A push with another name renames the tenant.
  • Memberships. A membership maps the person by (issuer, subject), never by email; a push whose external_user_id names a known user with a new subject moves that user to the new identity, so its history keeps its actor (a customer moving to its own identity provider). Billing's admin becomes an admin binding on the whole tenant, member a viewer binding; finer roles come from teams the tenant admins manage. revoked removes the membership with the user's bindings, tokens and team memberships in that tenant and ends the user's sessions there; the user and other tenants stay. Tenant admins invite and remove people through billing (/api/v1/billing/invitations, /api/v1/billing/revocations), which mails invitations and pushes the result.
  • Entitlements. The newest entitlement is stored on the tenant (tenant.entitlement). Only status: active makes the tenant writable; without an entitlement, or with any other status, the tenant is read-only: agents keep receiving their current configuration and their heartbeats are accepted, reads work, every other management write and the enrollment of a new agent get 403 tenant.read_only. The account handoff and revocations stay allowed. limits.agents of the entitlement becomes the tenant's agents limit. The replica holding controller shard 0 pulls every entitlement every 15 minutes in case a push was missed; without the billing service the last stored state holds.
  • Agent counts. Strataplane reports each tenant's agent count (the number the agents limit counts) to billing's PUT /api/v1/tenants/{id}/usage: every tenant hourly from the replica holding shard 0, and a tenant within seconds after an enrollment or a forgotten agent. Billing refuses a smaller plan the agents do not fit into.
  • Account handoff. POST /api/v1/billing/handoff returns a single-use link into the account UI for the calling tenant admin (plans, payment methods, invoices).

Strataplane calls the billing API with client-credentials tokens of its own machine user (billing.client_id, billing.client_secret_file, audience billing.billing_project_id).

Lifecycle. Creating a tenant creates its counters, its first data key, the empty revision r1, release pointers at r1 for every agent kind and the admins team. States and what they allow are in tenant_state. Deletion rejects all requests, deletes the tenant's rows, enqueues its object prefix for the blob sweeper, destroys its data keys, and finally removes the tenant row; the platform audit chain records each step.

Limits. tenant.limits caps what one tenant can use; defaults come from tenants.default_limits (operations.md §4):

LimitEnforced at
agentsEnrollment and class heartbeats creating an instance beyond it are rejected.
object_bytesJourney evidence uploads beyond it are rejected.
chat_bytesAssistant commits beyond it are rejected; deleting chats frees space.
api_rateManagement requests per second, counted per replica; 429 with Retry-After beyond it.

A rejected request returns the problem code tenant.limit_exceeded. GET /api/v1/tenant shows limits and usage. In service mode the agents limit comes from the entitlement.