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_REJECTEDerror and recovered from it - Archived an environment and watched the cascade prevent new writes
- Used
spl debug replayto inspect records in a specific namespace
You'll need: a running spl serve --daemon (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 daemon is healthy
spl doctorYou should see 7 PASS checks. If daemon reachable fails, run spl serve --daemon first.
spl namespace listOn 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' → activeTry creating a child without the project parent — you should get a clear refusal:
spl namespace create tutorial/web/stagingerror: parent namespace 'tutorial/web' does not exist or is not Active.
Create the parent first:
spl namespace create tutorial/webThis 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/stagingNamespace: tutorial/web/staging
Status: active
Level: ENV
Depth: 3
Description: Staging environment
Ancestor chain (root last):
✓ tutorial/web/staging
✓ tutorial/web
✓ tutorial
✓ defaultEvery 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 daemon 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-99Now 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' → archivedNow 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' → activeThe 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 activeStep 9: inspect the records you just created
Use spl debug replay to see the records on the staging thread:
spl debug replay th_tutorial_stagingspl 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 tutorialOr 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 tutorialThe 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:
- Parent validation — children require an Active parent (catches typos, prevents typing-mistake escalation)
- Explicit namespace registration — descendants don't inherit access automatically (real isolation, not glob patterns)
- Clear error messages — every rejection names the failing ancestor and includes the recovery command
- Lifecycle cascade — archiving a parent stops writes to every descendant immediately
- Sibling isolation — archiving one ENV doesn't touch siblings
- Re-activation by re-create — last-write-wins semantics, with the CLI flagging overrides
- Read-only inspection —
spl namespace showwalks ancestor chains,spl debug replaywalks 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
- Namespaces guide — production-grade setup, CEL rules, recovery procedures
- Namespaces concept — the WHY behind the design
- CEL Expressions —
current_namespace()for scoped permission rules - Debugging guide — when something doesn't work, this is the order to reach for tools
- Spec §08-governance/04 — normative source
Build your first workspace in 10 minutes
Scaffold, edit, test, publish, and share a recipe-collection workspace your friends can install. The complete happy path, end-to-end.
Your First Fan-Out
The 5-minute version. Boot a 3-instance fleet, fan out a trivial task to two workers, watch the join, inspect the speedup ratio, tear down. No real work, no LLM spend — just the shape of the thing.