Namespaces & Tenancy
Namespaces & Tenancy
In one sentence. A namespace is WIP's isolation and tenancy boundary — every document, template, and terminology carries a namespace (its prefix), and that Namespace record is what governs how cross-namespace references, ID generation, and deletion are allowed to behave for everything filed under it.
Why it exists. A Namespace is described in its own model as "a user-facing container for organizing data" (namespace.py), holding two things per tenant: isolation_mode, which decides whether other namespaces may reference into it at all, and id_config, which decides how identifiers are minted per entity type. It exists so that isolation and identity-generation policy are per-tenant configuration, not global platform behavior — and so a tenant can be removed (deletion_mode) without silently breaking tenants that still point into it (allowed_external_refs, and the inbound-reference checks run before deletion).
The model
Namespace (components/registry/src/registry/models/namespace.py) — Mongo collection namespaces, unique index on prefix, secondary index on status.
| Field | Type | Default | Meaning |
|---|---|---|---|
prefix | str | required | The namespace's unique key (e.g. "wip", "dev", "customer-abc") — enforced by a unique index (prefix_unique_idx) |
description | str | "" | Human-readable description |
isolation_mode | "open" | "strict" | "open" | "open" allows cross-namespace refs; "strict" requires refs to stay inside the namespace |
allowed_external_refs | list[str] | [] | In "open" mode, an optional allowlist of external namespace prefixes this namespace may be referenced from |
id_config | dict[str, Any] | {} | Per-entity-type ID-generation config, keyed by terminologies, terms, templates, documents, files. Read via get_id_algorithm(entity_type), which wraps a stored dict into an IdAlgorithmConfig, falling back to a platform default (DEFAULT_ID_CONFIG) and, failing that, to IdAlgorithmConfig(algorithm="uuid7") — the algorithm's own shape is out of this doc's scope; see Identity & the Registry |
deletion_mode | "retain" | "full" | "retain" | "retain" permits soft-delete only; "full" additionally permits hard-delete and full namespace deletion |
status | "active" | "archived" | "deleted" | "locked" | "active" | "locked" means a deletion is in progress; "deleted" is a legacy marker — see Gotchas |
created_at / created_by | datetime / str | None | now / None | Creation audit |
updated_at / updated_by | datetime / str | None | now / None | Last-update audit |
The model's own comment states the intended status lifecycle: active <-> archived (archive/restore), -> locked (deletion in progress), -> physical removal. The archive/restore transition itself is not implemented in this doc's scope — see the ⚠ GAP below.
Deletion touches more than the Namespace record. namespace_deletion.py enumerates every collection a namespace's data can live in:
| Collection | Database | Filter field |
|---|---|---|
files, documents | wip_document_store | namespace |
templates | wip_template_store | namespace |
terms, term_relations, term_audit_log, terminologies | wip_def_store | namespace |
registry_entries, namespace_grants, composite_key_claims | registry's own database (resolved at runtime via Beanie) | namespace |
id_counters | registry's own database | counter_key matched by regex ^<prefix>: — not a namespace field |
namespace_grants and composite_key_claims are referenced here only as raw collection names to delete from; their document shape is not modeled in this doc's scope.
How it works
Dry run. NamespaceDeletionService.dry_run(prefix) is read-only: it counts every entity in the table above (_count_entities), lists the namespace's MinIO object keys (_get_minio_keys), and checks inbound references from other namespaces (_check_inbound_references). It returns safe_to_delete (true only if no high-severity inbound reference exists) and requires_force.
Starting a deletion. start_deletion(prefix, force, requested_by):
- Loads the
Namespace; fails if it doesn't exist. - Refuses if
prefix == "wip"(the default namespace can never be deleted). - Refuses unless
deletion_mode == "full". - Refuses if
status == "locked"(a deletion is already in progress). - Re-checks inbound references; if any are high-severity and
forcewas not passed, refuses. - Sets
status = "locked"and saves the namespace. - Builds a
DeletionJournalof orderedDeletionSteps (_build_journal) and persists it. - Executes the journal (
_execute_journal).
Journal step order (_build_journal), each step only added if it has matching data:
- MinIO objects for the namespace's files — must run before file metadata is deleted.
- One step per non-empty collection in
_EXTERNAL_COLLECTIONS(document-store, template-store, def-store). - One step per non-empty collection in
_REGISTRY_COLLECTIONS(registry_entries,namespace_grants,composite_key_claims). id_counters, matched by thecounter_keyregex rather than anamespacefield.- A PostgreSQL step, only if
REPORTING_SYNC_URLis configured — deletes rows via a reporting-sync HTTPDELETEcall, not a direct DB connection. - A final step that deletes the
namespacesrecord itself.
Execution and crash recovery. _execute_journal runs steps in order, saving the journal after every step. A step already status == "completed" (from an earlier, interrupted run) is skipped. MongoDB step failures raise, mark the journal status = "failed", and stop the journal — per the module docstring, "Mongo steps fail the journal loudly and resume idempotently." MinIO and PostgreSQL step failures are swallowed into step.error instead of raising, and the step is marked "completed_with_errors" — per the module docstring, this is deliberate: "a dead secondary store must not make a namespace undeletable." resume_deletion(prefix) re-runs _execute_journal on the most recent DeletionJournal with status == "in_progress". recover_incomplete_deletions() runs the same resume for every in_progress journal at process startup.
Terminal status. Once every step has been attempted, the journal's overall status is set to "completed_with_warnings" if any step ended "completed_with_errors", otherwise "completed".
Rules & invariants
- The
wipnamespace is permanently protected.start_deletionraises unconditionally ifprefix == "wip"— this is a literal string check in the service, not a flag on theNamespacemodel. deletion_modegates hard deletion. A namespace defaults to"retain"(soft-delete only);start_deletionrefuses to run at all untildeletion_modeis switched to"full".- Deletion is single-flight per namespace.
status == "locked"blocks a secondstart_deletioncall outright. - Inbound-reference severity gates force.
template_extends(another namespace's template extends one of this namespace's templates) andterminology_reference(another namespace's template field points at one of this namespace's terminologies) are high severity and block deletion unlessforce=True.synonym_link(a registry synonym in another namespace pointing here) is low severity and never blocks — but it is still recorded in the journal'sbroken_referenceseven on a forced, non-checked path, because — per the code's own comment — "gating on force would drop the only record of what broke." - The dry-run count and the real deletion read the same collection lists.
_count_entitiesdeliberately iterates_REGISTRY_COLLECTIONS+_EXTERNAL_COLLECTIONS, the same lists_build_journaldeletes from, specifically so the two can't drift — the code's own comment notes this was previously a source of a bug ("composite_key_claims went uncounted while being deleted"). - Only MongoDB deletion is guaranteed. MinIO and PostgreSQL cleanup are best-effort by design, so a namespace can finish deletion (
"completed_with_warnings") with orphaned MinIO objects or PostgreSQL rows still present. A caller that treats"completed_with_warnings"as equivalent to full removal will be surprised — this is the kind of non-intuitive-friction behavior the PoNIFs doc catalogs; see See also. id_countersisn't namespace-scoped like everything else. Every other collection here is filtered on anamespacefield;id_countersis filtered by acounter_keystring-prefix regex instead, because counter documents are keyed"<prefix>:<entity_type>", not tagged with anamespacefield.
⚠ GAP: no permission/authorization check is visible anywhere in
namespace.pyornamespace_deletion.pyfor who may calldry_run,start_deletion, orresume_deletion. If such a check exists, it lives outside this doc'ssource_scope(likelyregistry/api/and/or the auth-gateway) — see Auth & Access.
Gotchas
status = "deleted"is dead. It remains a legal value in theLiteraland the model's own comment is explicit that it is retained "so pre-cutover records still load" — no code path in this doc's scope writes it. The live lifecycle isactive/archived/locked, notactive/deleted.- A "safe" dry run can still break something.
dry_run'ssafe_to_deleteis computed only from high-severity references (template_extends,terminology_reference); asynonym_linkreference from another namespace doesn't affectsafe_to_deleteorrequires_force, so a dry run can report "safe" while the real deletion still silently breaks a synonym link elsewhere. - A
nullcount from reporting-sync is guarded, not trusted. The PostgreSQL cleanup step explicitly checks for a present-but-nulltotal_deletedin the reporting-sync response and refuses to coerce it into the journal's int-typed count field, recording a degraded step instead — the code comment attributes this to a past reporting-sync response-shape change. - Archive/restore isn't in this file. The
statusdocstring documentsactive <-> archivedas a transition, but neithernamespace.pynornamespace_deletion.pycontains the code that performs it (see the⚠ GAPabove). namespace_grantsandcomposite_key_claimsare deleted as raw collections. Their schemas aren't modeled anywhere in this doc's scope — you only see them here as(collection_name, filter_field)pairs the deletion journal iterates.- A namespace with a stray cross-namespace synonym claim isn't fully covered by
start_deletion's own inbound-reference check. Thecomposite_key_claimscollection's own in-code comment notes that a claim owned by an entry in another namespace but referencing this one is handled separately, by an orphan-reconciliation routine that runs at startup — not by anything in_check_inbound_referencesor the deletion journal itself.
See also
- Data Model Reference — the documents, templates, and terminologies that live inside a namespace.
- Identity & the Registry —
id_config/IdAlgorithmConfig, identity hashing, and synonyms, which this doc treats only as a namespace-level configuration surface. - Registry API — the REST surface for namespace management (create, archive/restore, dry-run, delete) is deliberately out of this doc's scope; the manifest's own scope note assigns it there. (Not part of the reading list's literal fixed cross-ref set for concept docs — flagged for the reading list to confirm; see report.)
- Auth & Access — for the permission model this doc's scope does not establish (see
⚠ GAPabove). - PoNIFs — for the best-effort-completion behavior named in Rules & invariants.
