Federation & multi-tenancy
How multiple organizations share a single Verdifax deployment: what is scoped per API key today, what is not, and where the boundaries are, so a buyer's procurement team can review it.
TL;DR
Run reads are scoped to the API key that created the run, and,
from run 247 on, the MACC cumulative budget is bucketed by the
authenticated API key (api-key-N, or anonymous without a key).
Before run 247, the bucket was the caller-declared
attested_context.actor_id when one was supplied, falling back to
api-key-N, so a caller could start a fresh budget by declaring a
new actor. The manifest carries no tenant_id field; only the MACC
halt receipt records the bucket, as its tenant_id.
One deployment can serve several organizations, each with its own API keys. This separation is enforced by the server's access control, not cryptographically.
Identity model
| Concept | Source | Used by |
|---|---|---|
MACC bucket (tenant_id on a MACC halt receipt) | From run 247 on, the authenticated API key (api-key-N, or anonymous); before, the caller-declared attested_context.actor_id, falling back to api-key-N | MACC budget accounting |
actor_id | API key's NREP identity | Ed25519 signature on AER (made by Verdifax's server, which holds the private key), audit-log attribution |
attested_context.actor_id | Caller-declared in the request | Recorded in the run; from run 247 on it no longer chooses the MACC bucket |
api_key_id | Internal numeric key ID | Scoped queries (GetRunForKey), rate limits |
envelope_id | Generated per-run by DOG | Run-level chain-of-custody |
From run 247 on, each API key is its own MACC bucket: two keys (e.g. a customer's prod and staging keys) accumulate separately, and there is no grouping of several keys under one tenant.
Isolation guarantees
Run-level isolation
GET /runs/{id} and GET /runs?... are scoped to the
authenticated key. A request with API key A cannot read runs
created by API key B, even when both keys belong to the same
customer; the scope is per-key. This is defense-in-depth: even
if a key is compromised, only the runs it created are exposed.
# API key A creates run 142.
curl -X POST https://api.verdifax.com/execute \
-H "X-Verdifax-Key: $KEY_A" -d '{...}'
# Returns: { "run_id": 142 }
# API key B (different tenant) cannot see it.
curl https://api.verdifax.com/runs/142 \
-H "X-Verdifax-Key: $KEY_B"
# Returns: 404 Not Found (NOT 403, no enumeration leak)
Budget buckets (MACC)
The Multi-Agent Coordination Constraint coordinator tracks
cumulative tokens per budget bucket: from run 247 on, the
authenticated API key. The tokens it counts are caller-declared
(request_substance.cost.total_tokens); a run that reports none
counts as zero. Two keys have independent budget windows; one key
reaching its cap does not halt another key's runs. Before run
247, the bucket was the caller-declared
attested_context.actor_id when supplied, so a caller could choose
its bucket.
The at-execution cumulative state (pre and post cumulative tokens and cost, budget limits, window start) is sealed into every manifest under the current era, so auditors can see the bucket's recorded budget position at execution time without re-querying the orchestrator. The bucket's name itself is not in the manifest.
Erasure isolation (CRES)
POST /admin/erase is admin-only. It destroys a per-field
data-encryption key and issues a sealed deletion receipt bound to
the run. The encryption path is not yet wired into /execute, so
the destroyed key encrypts no stored field and the run's sealed
bundle is unchanged; it does not fulfil a right-to-be-forgotten
request today. The DEK store partitions keys by
(envelope_id, field_path); each envelope belongs to one run.
Operational topology
Single-cluster, multi-tenant (default)
┌────────────────────────┐
Tenant A keys ──▶│ │
│ Verdifax Orchestrator │
Tenant B keys ──▶│ (1 SQLite db, 1 Fly │──▶ Sigstore Rekor
│ machine pool) │
Tenant C keys ──▶│ │
└────────────────────────┘
This is the default and recommended topology. One Fly app, one SQLite database, every run indexed by the API key that created it. Suitable up to ~5 active tenants / ~1000 req/sec aggregate.
Single-cluster, sharded SQLite (V2)
Beyond ~5 tenants or ~1000 req/sec, the SQLite primary becomes a write bottleneck. The V2 path is to shard the RunStore by API key across multiple SQLite instances, each on its own Fly machine. Tracked as a follow-up; not required for current scale.
Per-tenant deployments (regulated industries)
Some buyers (defense, healthcare with locality requirements) need a Verdifax instance per tenant, not for technical isolation, but for legal isolation (data residency, sovereign-cloud requirements). Deploy the same Fly app configuration once per regulated tenant, in the region that tenant requires, and share the Sigstore Rekor anchor across deployments (Rekor is a public global log).
What to tell your auditors
The reassurance points buyers' compliance teams ask about:
- No cross-tenant reads through authenticated endpoints. Every
authenticated read endpoint is scoped per-API-key. The two
unauthenticated run endpoints,
GET /runs/{id}/bundle.jsonandGET /public/runs/{token}/bundle.json, serve full sealed bundles to anyone, but only for published runs: the runs a Verdifax admin key has published (unguessable token address). The 44 runs published under an earlierbuyer-demo-prefix rule at the numbered address were withdrawn on October 18, 2026. An admin key can publish only runs it created itself (it can unpublish any run), no tenant key can publish a run, and noroute_idmakes a run public. Every write run records the creating API key in the run row, which scopes every authenticated read. - Separate budget buckets per API key, from run 247 on.
MACC accounting windows are per API key, and the consumption they
count is caller-declared. Before run 247, a caller chose its
bucket by declaring
attested_context.actor_id. - Tenant identity is sealed, not asserted. The API key's actor identity is sealed into the manifest as the NREP actor fields. On records sealed under public-log leaf v5 (runs 236 onward), the NREP actor id, public key and signature are among the 33 values bound to the public-log entry, so rewriting such a run to attribute it to a different actor no longer matches the public log.
- Audit-PDF visibility is per-key. The audit PDF for run 142 is only retrievable by the API key that created run 142. The public verify page does not serve audit PDFs: it compares the values carried in its own link (Rekor log entry and AIVP governance values) with the bundle the API serves for a published run, and for a run that has not been published it retrieves nothing.
What multi-tenancy does NOT give you
- Per-tenant policy customization at runtime. PEPG policies are deployment-wide today. If tenant A needs a stricter policy than tenant B, that's a follow-up, tracked under the V2 per-tenant policy work.
- Per-tenant key rotation. Rotating MACC budget cycles or CRES KEKs per-tenant is not yet wired. The current model rotates everyone together. Tracked separately.
- Tenant-scoped log identity. Every run from this deployment
is anchored as a Sigstore Rekor
hashedrekordentry signed with the same Verdifax ECDSA P-256 log-signing key. Per-tenant signing keys require multiple deployments (the per-tenant topology above), and that's the right answer for tenants who insist on cryptographic separation of their proof history.
Migration path: single-tenant ⟶ multi-tenant
If you started single-tenant and now need to add a second tenant:
- Provision a new API key for the second tenant via
POST /admin/keyswith a uniquename. The orchestrator auto-generates anactor_idfor the new key. - Hand the secret to the new tenant out-of-band. They store
it as their
VERDIFAX_API_KEY. - Both tenants now share the deployment, with run reads scoped per key. No data migration, no cluster restart.
From run 247 on, the MACC budget for the new key starts at zero. Existing runs remain attributed to the API key that created them through the run record.
