Prefer Swagger UI? Click hereThe same API in the classic OpenAPI explorer.
// how-to · 12 min read

Share knowledge across tenants with Packs

The multi-tenant memory pattern: publish one non-secret base brain as a Pack, then give every per-tenant brain its own key and policy and mount that Pack in reference mode. You get a shared top with zero per-tenant re-seeding, a private layer sealed per tenant, and a one-call base update that every tenant sees at once. This guide walks the provisioning flow, updating the base, reseller layering, and the encryption split, with curl throughout.

Before you start

This guide assumes you have read the Brain Packs concept and have a Ringside API key with api:write. Brain Packs sit behind RINGSIDE_BRAIN_PACKS_ENABLED on top of RINGSIDE_BRAINS_ENABLED; until both are on the publish, packs and mounts endpoints return 404. Full endpoint detail is in the Brain Packs API reference.

This replaces "sub-brains"

If you were looking for parent_brain_id or brain inheritance to share a base across tenants: that proposal was superseded. The supported pattern is publish-a-Pack, mount-by-reference, which is exactly this guide.

The shape of the problem

You run one product over many tenants. Every tenant needs the same base of knowledge, your platform's runbooks, taxonomies and instincts, defined once. Every tenant also needs a private layer of its own: its own data, under its own encryption key, with its own memory policy. A single shared brain cannot do this, because the key and the policy are properties of the whole brain, not of a scope. Give each tenant its own brain and you get the private layer but lose the shared top, and re-seeding the base into thousands of brains by hand is a non-starter.

The pattern: keep the base in its own brain and publish it as a plaintext Pack. Give each tenant its own brain with its own key and policy, and mount the Pack underneath in reference mode. The base is stored once and read live by every tenant; each tenant's private writes stay sealed in its own brain. Update the base once and every tenant sees the new version. This is the scale case, the one Sentryx is built on: a small non-secret base under thousands of encrypted tenants.

        base brain (plaintext, non-secret)
                    |
              POST .../publish
                    v
            Pack  platform-runbooks  v2     <-- stored ONCE
              /         |          \
   reference mount  reference     reference
        |              |              |
   brn_acme        brn_globex     brn_initech     <-- each its own DEK + policy
   /shared (RO base)  /shared       /shared
   /acme   (private)  /globex       /initech
One Pack, stored once, mounted read-only under many per-tenant brains. Writes only ever land in each tenant's private scopes.

Step 1 — Publish the base brain as a plaintext Pack

Put your shared knowledge in a dedicated base brain (brn_base below) and curate it like any other: write reference docs and policies at its scopes, let it learn if you want. The base is not secret, it is the same for every tenant, so publish it with encryption_at_publish: "none". A reference mount requires a plaintext Pack.

curl https://api.fightclub.pro/v1/brains/brn_base/publish \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "name": "platform-runbooks", "slug": "platform-runbooks",
        "encryption_at_publish": "none", "notes": "initial base" }'
Publishing plaintext is a declassification

A reference mount needs a plaintext Pack, so anything in the base is stored in the clear. Publish refuses content that matches a secret pattern with 422 secret_detected rather than scrubbing it, but it is still your job to keep credentials and tenant-specific secrets out of the base. If the base brain is itself sealed, you must pass declassify: true to confirm you intend to expose its content as plaintext. Review the base before you publish it.

Note the pack_id. If the snapshot is large (over 1,000 entries) the call returns status: "building"; poll GET /v1/packs/{packId} until the version reads ready before any tenant mounts it.

Step 2 — Provision a tenant brain and mount the Pack

For each tenant, create a brain with its own encryption and its own memory policy, then mount the base Pack in reference mode. Set the shared taxonomy as the tenant brain's memory_policy at creation so its own learning is shaped the same way the base is. Embedding model and dimensions must match the Pack, so create the tenant brain with the same embedding_model you published the base with, or the mount is rejected with 422 embedding_model_mismatch.

curl https://api.fightclub.pro/v1/brains \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "name": "Acme tenant brain",
        "encryption": "standard",
        "embedding_model": "openai/text-embedding-3-small",
        "memory_policy": {
          "type_taxonomy": [
            { "type": "procedure", "tier": "A", "weight": 1.4 },
            { "type": "gotcha",    "tier": "A", "weight": 1.3 },
            { "type": "fact",      "tier": "B", "weight": 1.0 }
          ],
          "learn_mode": "autonomous"
        } }'
# -> { "id": "brn_acme...", "object": "brain", "encryption": "standard", ... }

The scope_map rebases the whole Pack under /shared, leaving the tenant's own scopes (/acme, /acme/q3, ...) free for private writes. A reference mount is status: "ready" immediately, so the very next recall composes both layers. Repeat this step per tenant: each call is one brain plus one mount, and nothing about the base is copied.

One reference mount per brain

A brain may hold at most one reference mount in v1 (a second returns 409 reference_mount_limit). That is exactly one shared base, which is the pattern here. materialize mounts are unlimited if you ever need extra imported bases under the same tenant.

Step 3 — Write and recall

Write the tenant's private memory to its own scopes as usual: every PUT /entries and every observe lands in the brain's native layer, never in the Pack. Recall composes the two in one ranked pass with no extra parameters. Pass explain: true to see which store each hit came from.

# private write to the tenant's own scope
curl -X PUT https://api.fightclub.pro/v1/brains/brn_acme/entries \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "scope": "/acme", "class": "memory", "type": "gotcha",
        "title": "Acme CDN", "body": "Acme rollback must flush the CDN cache first." }'

# recall sees the tenant's private memory AND the shared base, ranked together
curl https://api.fightclub.pro/v1/brains/brn_acme/recall \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "scope": "/acme", "query": "how do we roll back a failed deploy",
        "explain": true }'

The tenant's site-specific gotcha shadows or outranks the generic base procedure when they share scope and entity, so an agent acting on the recall gets the override before the default. The base is evergreen-scored (no recency or usage decay) under its own frozen tiers, while the tenant's memory ages and reinforces normally.

Step 4 — Update the base once, for everyone

When the shared knowledge changes, edit the base brain and publish again. Each publish mints a new immutable version.

curl https://api.fightclub.pro/v1/brains/brn_base/publish \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "notes": "Q3 runbook refresh" }'
# -> { "pack_id": "pack_7f3a2c...", "version": 2, "status": "ready", ... }

Mounts are pinned by default, so a tenant keeps reading the exact version it was mounted against until you move it. You have two ways to roll tenants forward:

  • ·Set follow_latest: true on a mount and a new publish re-points it instantly to the new version, with no per-tenant rewrite. Good for tenants you want always on the current base.
  • ·Or leave mounts pinned and move them deliberately with PATCH /v1/brains/{id}/mounts/{mountId} and { "version": 2 }. Good when you want to validate a new base on a few tenants before the fleet.
# opt a tenant into auto-updates
curl -X PATCH https://api.fightclub.pro/v1/brains/brn_acme/mounts/mnt_44c1 \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "follow_latest": true }'

# or move a pinned tenant to v2 on your schedule
curl -X PATCH https://api.fightclub.pro/v1/brains/brn_globex/mounts/mnt_77ab \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "version": 2 }'

Either way the base is rewritten exactly once, as one Pack version. For a reference mount the re-point is a metadata change, so even a fleet of thousands costs one publish plus cheap re-points, never a per-tenant re-seed.

Reseller and multi-level layering

A reseller sits between the platform and its own clients: platform knowledge at the bottom, the reseller's own shared layer in the middle, each client's private memory on top. Which design you use turns on one question, and getting it wrong is where people assume a three-level chain works when it does not. The question: do the reseller's clients need their own encryption key or their own memory policy?

Clients as scopes (the common case, supported today)

If the reseller's clients can share the reseller's key and policy, you do not need a brain per client. Give the reseller one brain, mount the platform Pack under it once, and use scopes for its clients: platform knowledge at the mounted base, reseller-wide knowledge at the reseller root, per-client memory under sub-scopes. Scope inheritance gives the shared top within one brain; the Pack mount adds the platform layer beneath all of it. This is still only two layers (the platform Pack plus the one reseller brain), so it composes today.

# one reseller brain, platform Pack mounted under /platform
curl https://api.fightclub.pro/v1/brains/brn_reseller/mounts \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "pack_id": "pack_7f3a2c", "mode": "reference",
        "scope_map": { "/": "/platform" } }'

# reseller-wide note at the root; per-client memory under sub-scopes
curl -X PUT https://api.fightclub.pro/v1/brains/brn_reseller/entries \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "scope": "/", "class": "policy", "title": "House style",
        "body": "All client comms go out in the reseller brand voice." }'

# recall for one client sees: client scope + reseller root + platform base
curl https://api.fightclub.pro/v1/brains/brn_reseller/recall \
  -H "Authorization: Bearer $RINGSIDE_KEY" \
  -d '{ "scope": "/clients/dunder", "query": "deploy rollback policy" }'

Clients as their own brains: a known v1 limit

When a client genuinely needs its own DEK or its own memory_policy, it must be its own brain, not a scope. That client now needs three layers composed at recall: the platform base, the reseller's shared layer, and its own private memory. That is two distinct shared layers under one brain, and v1 cannot stack two live shared layers. Two rules combine to prevent it. Publish snapshots only the publishing brain's own native rows, never the bases it itself mounts, so a reseller cannot reference-mount the platform Pack, add its layer, and republish the union (the new Pack would contain only the reseller's own rows). And a brain may hold at most one reference mount, so a client cannot reference-mount the platform Pack and a reseller Pack at the same time. Mounting only the platform Pack from the client brain gives platform plus the client's own memory but loses the reseller's shared layer, which is the trap.

Workaround today: materialize the second shared layer

The client brain reference-mounts the platform Pack (live, zero-copy, stored once globally) and materialize-mounts the reseller Pack (an imported copy, storage-rate, no re-embed) on top of its own native memory. Recall composes all three. The largest, most-shared layer stays zero-copy live for everyone; only the smaller reseller layer is copied per client, across a bounded client count. Updating the platform base auto-propagates via follow_latest; updating the reseller Pack re-materializes each client mount.

Roadmap: native multi-level (include_mounts)

Native stacking of two live shared layers is planned as an include_mounts flag on publish: the reseller publishes platform plus its own as one self-contained Pack the client reference-mounts, so it stays a single live layer with one scan and the platform-versus-reseller conflict curated by the reseller at publish time, not adjudicated by an engine rule at recall. Raising the one-reference-mount cap, to compose layers live at recall, is the deferred general alternative, for when independent knowledge dimensions (vertical, compliance profile, reseller) must be composed without pre-flattening. Until either ships, scopes cover reseller clients that share a key and the materialize workaround covers clients that need their own. None of this affects the two-level platform-to-tenant pattern, which is fully supported.

The encryption split

The whole pattern rests on putting encryption where it earns its keep: on the private layer, not the shared base.

The base is plaintext and non-secret
It is your runbooks and instincts, identical for every tenant, published with encryption_at_publish=none so it can be read live by reference mounts. Sealing it under each tenant's key would buy zero security and cost one full copy per tenant.
The private layer is sealed per tenant
Each tenant brain has its own DEK and its own memory_policy. Its server topology, procedures and gotchas live here, sealed under its own key, untouched by the shared base.
Real secrets live in a secret store
Raw credentials, keys and tokens never belong in a brain or a Pack at all. Publish refuses content that matches a secret pattern (422 secret_detected). Keep secrets in a real secret store and reference them from your application, not from memory.
When a tenant wants even the base under its own key

Some tenant insists the shared layer be sealed under its own key, or a cross-account consumer wants a self-contained copy? Mount the Pack in materialize mode instead: it imports the Pack's rows into that tenant's brain, re-sealed under the tenant key. You pay one re-sealed copy per such tenant and a re-import on each base update, which is exactly why reference is the default for the scale case and materialize is the premium isolation tier.

Recap

  1. 01Curate a non-secret base brain and publish it as a plaintext Pack (encryption_at_publish: "none").
  2. 02Per tenant: create a brain with its own encryption, matching embedding model, and the shared taxonomy as its memory_policy.
  3. 03Mount the Pack in reference mode with a scope_map that tucks the base under /shared (or /platform).
  4. 04Write private memory to the tenant's own scopes; recall composes both layers automatically.
  5. 05Update the base by re-publishing; roll tenants forward with follow_latest or a deliberate PATCH.