← Back to Library

Namespaces & Tenancy

concept ·  operators, modelers

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.

FieldTypeDefaultMeaning
prefixstrrequiredThe namespace's unique key (e.g. "wip", "dev", "customer-abc") — enforced by a unique index (prefix_unique_idx)
descriptionstr""Human-readable description
isolation_mode"open" | "strict""open""open" allows cross-namespace refs; "strict" requires refs to stay inside the namespace
allowed_external_refslist[str][]In "open" mode, an optional allowlist of external namespace prefixes this namespace may be referenced from
id_configdict[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_bydatetime / str | Nonenow / NoneCreation audit
updated_at / updated_bydatetime / str | Nonenow / NoneLast-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:

CollectionDatabaseFilter field
files, documentswip_document_storenamespace
templateswip_template_storenamespace
terms, term_relations, term_audit_log, terminologieswip_def_storenamespace
registry_entries, namespace_grants, composite_key_claimsregistry's own database (resolved at runtime via Beanie)namespace
id_countersregistry's own databasecounter_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):

  1. Loads the Namespace; fails if it doesn't exist.
  2. Refuses if prefix == "wip" (the default namespace can never be deleted).
  3. Refuses unless deletion_mode == "full".
  4. Refuses if status == "locked" (a deletion is already in progress).
  5. Re-checks inbound references; if any are high-severity and force was not passed, refuses.
  6. Sets status = "locked" and saves the namespace.
  7. Builds a DeletionJournal of ordered DeletionSteps (_build_journal) and persists it.
  8. Executes the journal (_execute_journal).

Journal step order (_build_journal), each step only added if it has matching data:

  1. MinIO objects for the namespace's files — must run before file metadata is deleted.
  2. One step per non-empty collection in _EXTERNAL_COLLECTIONS (document-store, template-store, def-store).
  3. One step per non-empty collection in _REGISTRY_COLLECTIONS (registry_entries, namespace_grants, composite_key_claims).
  4. id_counters, matched by the counter_key regex rather than a namespace field.
  5. A PostgreSQL step, only if REPORTING_SYNC_URL is configured — deletes rows via a reporting-sync HTTP DELETE call, not a direct DB connection.
  6. A final step that deletes the namespaces record 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

⚠ GAP: no permission/authorization check is visible anywhere in namespace.py or namespace_deletion.py for who may call dry_run, start_deletion, or resume_deletion. If such a check exists, it lives outside this doc's source_scope (likely registry/api/ and/or the auth-gateway) — see Auth & Access.

Gotchas

See also

Generated from real source code, not hand-written — see the WIP Technical Library.