SSyncropel Docs

Your First Multi-Namespace Setup

Walk through setting up two isolated environments under one ORG, writing records into each, and verifying the narrowing rule in action — about 15 minutes.

What you'll learn

By the end of this tutorial you'll have:

  • Created an ORG-level namespace and two ENV-level children
  • Written records into each environment via the HTTP API
  • Confirmed that records from one environment can't accidentally claim the other's namespace
  • Triggered the 403 NAMESPACE_REJECTED error and recovered from it
  • Archived an environment and watched the cascade prevent new writes
  • Used spl debug replay to inspect records in a specific namespace

You'll need: a running spl serve (the Quickstart covers this), and curl. About 15 minutes.

If you skipped the Namespaces concept page, read the first three sections first — they explain the WHY this tutorial demonstrates.

Step 1: confirm the instance is healthy

spl doctor

You should see 7 PASS checks. If instance reachable fails, run spl serve first.

spl namespace list

On a fresh install this shows just default (the implicit root). That's our starting point.

Step 2: create the ORG

We'll set up a fictional company called "tutorial" with staging and prod environments under a web project.

spl namespace create tutorial --description "Tutorial walkthrough tenant"
✓ namespace 'tutorial' → active

Try creating a child without the project parent — you should get a clear refusal:

spl namespace create tutorial/web/staging
error: parent namespace 'tutorial/web' does not exist or is not Active.
  Create the parent first:
    spl namespace create tutorial/web

This is the parent-validation pre-flight. The CLI catches the orphan attempt before any record is written.

Step 3: walk down the tree

spl namespace create tutorial/web --description "Web project"
spl namespace create tutorial/web/staging --description "Staging environment"
spl namespace create tutorial/web/prod --description "Production environment"

Verify the structure:

spl namespace list
  ID                                  STATUS     LEVEL      DESCRIPTION
  default                             active     DEFAULT    System default namespace (implicit)
  tutorial                            active     ORG        Tutorial walkthrough tenant
  tutorial/web                        active     PROJECT    Web project
  tutorial/web/prod                   active     ENV        Production environment
  tutorial/web/staging                active     ENV        Staging environment

  4 explicit + 1 implicit (default)

Inspect the staging env to see its full ancestor chain:

spl namespace show tutorial/web/staging
Namespace: tutorial/web/staging
  Status:      active
  Level:       ENV
  Depth:       3
  Description: Staging environment

  Ancestor chain (root last):
    ✓ tutorial/web/staging
    ✓ tutorial/web
    ✓ tutorial
    ✓ default

Every ancestor is Active (). Records claiming tutorial/web/staging will be accepted.

Step 4: write your first namespace-scoped record

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:tutorial",
    "thread": "th_tutorial_staging",
    "body": {
      "namespace": "tutorial/web/staging",
      "goal": "Deploy v1.0 to staging"
    },
    "clock": 0,
    "data_type": "VOID"
  }'

You should see a successful response with the new record ID:

{
  "object": "record",
  "id": "abc123...",
  "thread": "th_tutorial_staging",
  "actor": "did:sync:user:tutorial",
  "act": "INTEND",
  ...
}

Now write a follow-up DO record:

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "DO",
    "actor": "did:sync:user:tutorial",
    "thread": "th_tutorial_staging",
    "body": {
      "namespace": "tutorial/web/staging",
      "topic": "task_started",
      "action": "deploy"
    },
    "clock": 1,
    "data_type": "VOID"
  }'

Step 5: trigger the namespace rejection

Now try to write a record claiming a namespace that doesn't exist:

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:tutorial",
    "thread": "th_tutorial_test",
    "body": {
      "namespace": "fake-corp",
      "goal": "should fail"
    },
    "clock": 0,
    "data_type": "VOID"
  }'

You should get a 403 with a helpful error message:

{
  "object": "error",
  "type": "invalid_request_error",
  "code": "NAMESPACE_REJECTED",
  "message": "namespace 'fake-corp' rejected: ancestor 'fake-corp' is not Active in the registry. Create it first with `spl namespace create fake-corp`."
}

The error tells you exactly what's wrong AND how to fix it. This is the monotonic narrowing rule in action — the instance walked the ancestor chain, hit fake-corp, found it missing from the registry, and refused the write.

Step 6: try cross-environment escalation

Now try something more interesting. Write a record claiming tutorial/web/staging/job-99 — a JOB-level namespace under your existing staging environment, but one you didn't explicitly create:

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:tutorial",
    "thread": "th_tutorial_jobtest",
    "body": {
      "namespace": "tutorial/web/staging/job-99",
      "goal": "should also fail"
    },
    "clock": 0,
    "data_type": "VOID"
  }'
{
  "object": "error",
  "code": "NAMESPACE_REJECTED",
  "message": "namespace 'tutorial/web/staging/job-99' rejected: ancestor 'tutorial/web/staging/job-99' is not Active in the registry. Create it first with `spl namespace create tutorial/web/staging/job-99`."
}

This is important: a parent namespace doesn't auto-grant access to its descendants. Just because tutorial/web/staging is Active doesn't mean any child namespace under it is automatically allowed. Each namespace must be explicitly created. This is what makes the registry a real isolation boundary instead of a glob pattern.

To allow job-99, create it:

spl namespace create tutorial/web/staging/job-99

Now retry the curl — it should succeed.

Step 7: archive an environment

Imagine staging is being decommissioned. Archive it:

spl namespace archive tutorial/web/staging
✓ namespace 'tutorial/web/staging' → archived

Now any new write claiming tutorial/web/staging (or any descendant, including the job-99 you just created) is rejected:

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:tutorial",
    "thread": "th_tutorial_postarchive",
    "body": {"namespace": "tutorial/web/staging", "goal": "should fail"},
    "clock": 0, "data_type": "VOID"
  }'
{
  "code": "NAMESPACE_REJECTED",
  "message": "namespace 'tutorial/web/staging' rejected: ancestor 'tutorial/web/staging' is not Active in the registry."
}

And the cascade catches the descendant too:

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:tutorial",
    "thread": "th_tutorial_postarchive2",
    "body": {"namespace": "tutorial/web/staging/job-99", "goal": "cascade test"},
    "clock": 0, "data_type": "VOID"
  }'
{
  "code": "NAMESPACE_REJECTED",
  "message": "namespace 'tutorial/web/staging/job-99' rejected: ancestor 'tutorial/web/staging' is not Active in the registry."
}

The error names tutorial/web/staging (the archived ancestor) as the failing link, not job-99 itself. The descendant inherits its parent's archived state instantly.

tutorial/web/prod is still fine because its ancestor chain (tutorial/web/prod → tutorial/web → tutorial → default) is still all Active. Try writing to it:

curl -X POST http://localhost:9100/v1/records \
  -H "Content-Type: application/json" \
  -d '{
    "act": "INTEND",
    "actor": "did:sync:user:tutorial",
    "thread": "th_tutorial_prod",
    "body": {"namespace": "tutorial/web/prod", "goal": "still works"},
    "clock": 0, "data_type": "VOID"
  }'

Success. Sibling environments are isolated — archiving staging doesn't touch prod.

Step 8: re-activate by re-creating

Decided you need staging back? Just re-create it:

spl namespace create tutorial/web/staging --description "Revived"
namespace 'tutorial/web/staging' already exists and is Active — overriding
✓ namespace 'tutorial/web/staging' → active

The CLI tells you it's overriding the prior state (which was Archived). The latest record per namespace ID wins, so the new Active LEARN supersedes the Archived one.

Note: re-activating the parent does NOT re-activate descendants. If you also archive tutorial/web/staging/job-99 and want it back, you have to re-create it separately:

spl namespace show tutorial/web/staging/job-99
# Status: archived
spl namespace create tutorial/web/staging/job-99
# Now active

Step 9: inspect the records you just created

Use spl debug replay to see the records on the staging thread:

spl debug replay th_tutorial_staging
spl debug replay th_tutorial_staging

  CLOCK  ACT    ACTOR                               STATUS       BODY
      0  INTEND did:sync:user:tutorial              inbox        goal=Deploy v1.0 to staging
→     1  DO     did:sync:user:tutorial              active       topic=task_started

  Final status: active (2 records)

The replay walks the records in clock order and shows the derived status after each one. The arrow marks status transitions.

Step 10: clean up

When you're done with the tutorial, archive (or delete) the namespaces you created:

spl namespace archive tutorial/web/staging/job-99
spl namespace archive tutorial/web/staging
spl namespace archive tutorial/web/prod
spl namespace archive tutorial/web
spl namespace archive tutorial

Or for a hard cleanup (still preserves audit trail):

spl namespace delete tutorial/web/staging/job-99
spl namespace delete tutorial/web/staging
spl namespace delete tutorial/web/prod
spl namespace delete tutorial/web
spl namespace delete tutorial

The records you wrote during the tutorial (in th_tutorial_staging, th_tutorial_prod, etc.) remain in the store as part of the immutable record log. Threads aren't deleted by archiving namespaces — namespaces gate write access, not data retention.

What you learned

Going through this tutorial you've exercised:

  1. Parent validation — children require an Active parent (catches typos, prevents typing-mistake escalation)
  2. Explicit namespace registration — descendants don't inherit access automatically (real isolation, not glob patterns)
  3. Clear error messages — every rejection names the failing ancestor and includes the recovery command
  4. Lifecycle cascade — archiving a parent stops writes to every descendant immediately
  5. Sibling isolation — archiving one ENV doesn't touch siblings
  6. Re-activation by re-create — last-write-wins semantics, with the CLI flagging overrides
  7. Read-only inspectionspl namespace show walks ancestor chains, spl debug replay walks records

These are the building blocks of a production multi-tenant deployment. For real setups, see the Namespaces guide which covers CEL rules scoped to namespaces, the operational checklist before turning on enforcement, and recovery procedures.

Next steps

On this page