CortexDB Docs
Docs for CortexDB 0.14.0, the version running on CortexDB Cloud (api.cortexdb.ai).
Shared Memory

Change Sets

Stage access changes, preview who gains and loses access, and activate them at once or on a date, with the tenant's own assertions as a guard.

A change set rewrites a tenant's access records as one change. You stage it in parts (nothing is visible yet), preview its effect (which records change, who gains and who loses access), and activate it with the preview's hash, now or on a date. Activation applies every part at once, or nothing.

When to use it

Use a change set for anything bigger than one roster edit: a reorg, onboarding a team, a new application's admissions, a legal hold, a policy record. It is also how you write the newer record types (sets, holds, policies, companion records) and roster fields (summary, until, when, a set as a member), which the older registry routes do not take. Those routes (POST /v1/scopes, PUT /v1/scopes/members, PUT /v1/scopes/tree) still work and make one-part changes with no preview.

The lifecycle

StepRouteWhat happens
StagePOST /v1/access/changes (more parts: POST /v1/access/changes/{id}/parts)The parts are stored, not applied. The id is yours, unique per tenant root and caller, so a retry finds the first attempt
PreviewPOST /v1/access/changes/{id}/previewThe access diff against the records as they are now, with a structural preview_hash
ActivatePOST /v1/access/changes/{id}/activateApplies every part, if the hash still matches and every check passes. Returns a consistency token
StatusGET /v1/access/changes/{id}?scope=<root>staged, scheduled, activated or discarded
DiscardDELETE /v1/access/changes/{id}?scope=<root>Drops a staged or scheduled change set

Activation refuses, and changes nothing, when:

  • the structure moved since the preview: 409 PREVIEW_CHANGED, with the current hash;
  • a record it writes changed since it was staged: 409 CHANGESET_STALE (discard it and stage again);
  • a change that widens access breaks one of the tenant's assertions: 409 ACCESS_ASSERTION_FAILED;
  • it would pass a deployment limit: 409 ACCESS_LIMIT_REACHED;
  • the caller no longer has the authority to make it: 403 POLICY_DENIED.

Who may call. For every record a change set writes, the caller is an Owner of it with policy.administer.scope there; a new record needs scope.create.<type> and Ownership of its parent. An Owner of the tenant root covers everything in the tenant. The operator key may always stage; under the deny operator mode its activation of a change that registers a record, names a new service: principal or widens a private region needs a break-glass header. A change set never reaches another tenant.

Example: add a team

Stage a change set that registers org:acme/team:eng with Bob as Owner and Alice as Reader:

curl -sS -X POST "$CORTEX_URL/v1/access/changes" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{
  "scope": "org:acme",
  "id": "add-eng-team",
  "reason": "Add the engineering team",
  "parts": [{"part": 0, "records": [
    {"path": "org:acme/team:eng", "op": "put", "record": {
      "members": [
        {"actor": "user:bob", "role": "owner"},
        {"actor": "user:alice", "role": "reader", "reason": "reads engineering decisions"}
      ],
      "policies": {}, "auto_provisioned": false, "created_at": "2026-10-29T00:00:00Z",
      "tree": {"kind": "team", "parent": "org:acme"}
    }}
  ]}]
}'
# {"id":"add-eng-team","scope":"org:acme","state":"staged","parts":1,"parts_total":1,…}

Each record is the stored record, in the shape GET /v1/scopes?path= returns: members, policies, auto_provisioned and created_at are required, and the server sets created_at, auto_provisioned and billing_tenant itself. A record with "op": "delete" and no record removes one. A reason (on the change set or on an entry) is at most 256 bytes and holds no personal data.

Preview it, with the people:

curl -sS -X POST "$CORTEX_URL/v1/access/changes/add-eng-team/preview" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{"scope": "org:acme", "people": true}'
{
  "id": "add-eng-team",
  "preview_hash": "sha256:02d20b6c98b20ab0feeadc721fb76b9912418ae8c3375ded6dc5d8517f735f73",
  "widens": true, "narrows": false,
  "summary": {"records_written": 1, "units": 1, "people_gaining": 2, "people_losing": 0, "rollups_hidden": 0},
  "units": [{"unit": "org:acme/team:eng", "records": ["org:acme/team:eng"],
             "people": [
               {"principal": "user:alice", "gains": [{"scope": "org:acme/team:eng", "before": null, "after": "reader"}], "losses": []},
               {"principal": "user:bob", "gains": [{"scope": "org:acme/team:eng", "before": null, "after": "owner"}], "losses": []}]}],
  "assertions": {"evaluated": 0, "failed": []},
  "stale_paths": [],
  "in_band": true,
  "in_band_parts": [{"part": 0, "path": "org:acme/team:eng", "reason": "registers"}],
  "has_more": false, "next_cursor": null
}

(The response also lists each record's structural change, trimmed here.) in_band: true says that an activation of this change set by the operator key is an in-band action, which needs a break-glass header under the deny mode. Activate with the hash you reviewed:

curl -sS -X POST "$CORTEX_URL/v1/access/changes/add-eng-team/activate" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{"scope": "org:acme", "preview_hash": "sha256:02d20b6c98b20ab0feeadc721fb76b9912418ae8c3375ded6dc5d8517f735f73"}'
# {"id":"add-eng-team","state":"activated","revision":3,…,"consistency_token":"ct1.eyJy…"}

A wrong or out-of-date hash is refused, and nothing is activated:

{"error_code": "PREVIEW_CHANGED", "message": "the change set's structural effect is not the one approved",
 "details": {"preview_hash": "sha256:02d20b6c…"}, "retriable": false}

The hash covers the structure only (kinds, parents, privacy, audiences, labels, admissions), not the people on rosters, so roster churn inside the touched units does not invalidate an approval; the preview's people show who those changes reach.

Assertions: who must never see what

A policy record holds the tenant's assertions: cases of "this principal at this scope must, or must never, hold this role". Stage it alone, in a change set of its own:

curl -sS -X POST "$CORTEX_URL/v1/access/changes" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{
  "scope": "org:acme", "id": "privacy-assertions",
  "parts": [{"part": 0, "records": [
    {"path": "org:acme/policy:privacy", "op": "put", "record": {
      "members": [{"actor": "user:admin", "role": "owner"}],
      "policies": {}, "auto_provisioned": false, "created_at": "2026-10-29T00:00:00Z",
      "tree": {"kind": "policy", "parent": "org:acme"},
      "policy": {"cases": [
        {"principal": {"actor": "user:bob"}, "scope": "org:acme/user:alice",
         "role": "reader", "expect": "deny", "note": "Bob never reads Alice private memory"}
      ]}
    }}
  ]}]
}'

Preview and activate it as above. From then on a change set that would give Bob Reader on Alice's private region previews with the case under assertions.failed, and its activation is refused:

{"error_code": "ACCESS_ASSERTION_FAILED", "message": "the change widens access past the tenant's assertions",
 "details": {"failed": [{"case": "0", "policy": "org:acme/policy:privacy", "widening": true}]}, "retriable": false}

A change is blocked only when it widens access and breaks a case that held before it. A change that does not widen activates, and watch lists the cases it breaks.

Dated activation

Add activate_at (RFC 3339, at most 90 days ahead) when you stage. Activating it before the date records your approval and answers 202 with state scheduled; the server activates it at the date, running every check again, and leaves it staged with a refusal if a check fails then.

curl -sS -X POST "$CORTEX_URL/v1/access/changes" \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H "Content-Type: application/json" \
  -d '{"scope": "org:acme", "id": "add-sales-team", "activate_at": "2026-11-15T09:00:00Z",
       "parts": [{"part": 0, "records": [{"path": "org:acme/team:sales", "op": "put", "record": {
         "members": [{"actor": "user:admin", "role": "owner"}], "policies": {},
         "auto_provisioned": false, "created_at": "2026-10-29T00:00:00Z",
         "tree": {"kind": "team", "parent": "org:acme"}}}]}]}'
# then preview and activate as above:
# 202 {"id":"add-sales-team","state":"scheduled","activate_at":"2026-11-15T09:00:00Z","approved_preview_hash":"sha256:…",…}

A staged or scheduled change set that is never activated is discarded 7 days after its date (or its staging, when undated).

Narrowing change sets

"narrowing": true declares that a change set only removes access; one whose records would widen access is refused at staging (422 INVALID_BODY, not_narrowing, naming the first record that widens). Narrowing change sets have a staging quota of their own, so a removal is never held back behind someone else's large staging, and they are admitted even while the disk is critical.

See also

  • Org Trees · Erasure and Legal Holds (a hold is a change set too)
  • GET /v1/access/watch?scope=<root> lists a root's registry changes, change-set events and notices from a position, for an audit feed or a cache that must follow access.

On this page