SSyncropel Docs

Namespaces

A 5-level hierarchy that scopes every record to a tenancy, project, environment, or job — with monotonic narrowing of capabilities from parent to child.

Overview

A namespace is a scope. Every record in Syncropel belongs to exactly one namespace, and namespaces form a 5-level hierarchy where each level narrows the capabilities of the level above. Parent grants are inherited; children can only refine, never expand.

This is how Syncropel handles tenancy (one ORG can't see another's records), project isolation (your payments work doesn't leak into your analytics work), environment separation (staging and production are two different worlds), and per-job scoping (a single dispatch run gets its own JOB-level container that vanishes cleanly when done).

If you skip this concept entirely, you get the implicit default namespace and everything Just Works. You only need to think about namespaces when your team gets bigger than one identity, when you want to separate a customer's data from your own, or when you're enforcing per-environment policies.

The 5 levels

DEFAULT → ORG → PROJECT → ENV → JOB
LevelExample IDPurposeWhen you'd create one
DEFAULTdefaultSystem-wide baseline. Implicit, can't be deleted.Never — it always exists.
ORGacme-corpTenant boundary. One company / team / customer.When you onboard a second customer or split your team into independent units.
PROJECTacme-corp/paymentsA workstream within an org.When two projects under the same org need independent budgets, dial settings, or rule sets.
ENVacme-corp/payments/stagingA stage within a project.When you have staging and production for the same project and you want CEL rules that only fire in one.
JOBacme-corp/payments/staging/job-42A per-execution scope.When a long-running dispatch needs its own ephemeral scope that can be cleaned up cleanly.

The maximum depth is 4 segments under DEFAULT. You can stop at any level — acme-corp (just ORG) is a complete, valid namespace.

How records get a namespace

Every record carries its namespace in body.namespace:

{
  "act": "INTEND",
  "actor": "did:sync:user:alice",
  "thread": "th_payments_deploy",
  "body": {
    "namespace": "acme-corp/payments/staging",
    "goal": "Deploy v2.3 to staging"
  },
  "clock": 0,
  "data_type": "VOID"
}

Records that omit body.namespace fall through to the implicit default. Every existing record continues working — default is always Active and always allowed.

Lifecycle

A namespace has three states:

  • Active: accepts records, enforces policy. The normal state.
  • Archived: read-only. No new records may target this namespace or any descendant. The audit history is preserved.
  • Deleted: soft delete. Treated as nonexistent for ingest, but kept in the registry for audit trail.

Transitions:

        spl namespace create

         Active ──→  Archived  ──→  Deleted
                  ←──
                spl namespace
                  unarchive
                  (re-create)

You can re-activate an archived namespace by running spl namespace create again with the same ID — the latest record per ID wins, so a new Active LEARN supersedes the prior Archived one.

The narrowing rule

The single most important property of namespaces: a child can never claim capabilities its parent doesn't grant. This is checked at every record ingest. The kernel walks the ancestor chain from the record's claimed namespace up to DEFAULT, and rejects the record if any link is missing or non-Active.

Record claims:   acme-corp/payments/staging/job-42

Walk:            acme-corp/payments/staging/job-42  →  Active?  ✓
                 acme-corp/payments/staging          →  Active?  ✓
                 acme-corp/payments                  →  Active?  ✓
                 acme-corp                           →  Active?  ✓
                 default                             →  Active?  ✓ (always)

Decision:        ACCEPT

But:

Record claims:   acme-corp/payments/staging/job-42

Walk:            acme-corp/payments/staging/job-42  →  Active?  ✓
                 acme-corp/payments/staging          →  Active?  ✓
                 acme-corp/payments                  →  Active?  ✗  (Archived)

Decision:        403 NAMESPACE_REJECTED
                 "namespace 'acme-corp/payments/staging/job-42' rejected:
                  ancestor 'acme-corp/payments' is not Active in the registry."

This is monotonic narrowing. Archiving an org cascades to every descendant immediately. There's no "hole" through which a deeper namespace can keep writing after its parent has been turned off.

Why this design

Three concrete reasons the spec mandates this shape:

1. Predictable cleanup. When a customer leaves or a project ends, you archive the ORG-level namespace and you're done. Every JOB, ENV, and PROJECT record under it stops accepting writes. You never have to walk the descendant tree by hand or write cleanup queries against the database.

2. Tenancy without forking the kernel. The same daemon serves multiple orgs without cross-contamination. An attacker who somehow obtains an ORG-level credential for acme-corp cannot inject records claiming bigcorp as their namespace — the registry doesn't have a bigcorp ancestor for them to pass through.

3. Policy composition is closed. When (in a future release) namespaces also carry policy YAML, the resolved policy at any level is the intersection of every ancestor's policy. There is no way to add capability by going deeper. This makes audit feasible — you can answer "what could a job-42 dispatch ever do?" by looking at the chain of policies, no need to chase implicit grants.

When NOT to use namespaces

  • You're a solo developer running spl on your laptop. Stay in default. There's nothing to gain.
  • You have one team and one production environment. A single ORG is overkill — default is fine.
  • You're prototyping. Namespaces are about boundaries, and prototyping is about not having boundaries. Add namespaces when the boundaries become real (a second customer, a second product, a regulated environment).

The implicit default namespace is always there, always Active, and never goes away. The namespace system is fully backward compatible with every pre-namespace record — you only think about namespaces when you actively need them.

What's enforced today vs the spec

The current implementation covers the existence layer of the narrowing guarantee: every record's claimed namespace and every ancestor must be Active in the registry. This is enforced at the HTTP boundary by the daemon's validate_namespace_for_ingest check.

Two pieces from the spec §08-governance/04 are not yet implemented and are tracked as follow-up tasks:

  • Policy composition — the YAML policy language with dial, primitives, max_effects, etc. and the intersect-on-allow / union-on-deny composition rules. Today, the policy field on a namespace entry is opaque text. The narrowing check only validates existence + status, not capability subsets.
  • Store-level isolation — the spec mandates that backends partition records by namespace at the storage layer for hard isolation. Today, records are tagged but not indexed by namespace, and read queries return records from all namespaces a caller has access to.

Both are bounded follow-ups, not show-stoppers. The HTTP-boundary enforcement is what most operators actually need: an attacker on the network cannot inject records into namespaces that don't exist.

Next steps

  • Use it now: see spl namespace in the CLI reference for the create/list/show/archive/delete commands.
  • Author CEL rules scoped to namespaces: the current_namespace() function and record.body.namespace field are both available in every CEL context. See CEL Expressions.
  • Read the spec: §08-governance/04 Namespaces is the normative source.
  • Walk through the worked example: see the Namespaces guide for a full setup from spl namespace create to a working multi-environment deployment.

On this page