Persist the Organization to AR namespace mapping on the Rails side

Summary

The monolith owns and persists the Organization to AR-namespace mapping, populated at provisioning. This keeps AR's API free of Organization concepts and removes per-request round-trips to resolve an organization to its namespace.

Decided. Captured in ADR-022, ADR-014, and ADR-007 via handbook!20291, with the internal API surface in ADR-009 via handbook!20309. This issue tracks the Rails-side implementation.

Not a data-ownership violation

The mapping holds only the minimal reference the monolith needs to reach its entry point into AR: the AR namespace id for an organization. This is a pointer to a dependency (like a foreign key to a downstream service), not a copy of AR's data. AR stays the sole owner; the monolith resolves anything further by the namespace id, never by organization.

Background

AR is decoupled from Organizations (ADR-022): the namespaces table references its owner only through an opaque soft FK (entity_type, entity_id), with no foreign key to organizations. AR owns the id (uuid PK) and slug. The current schema already guarantees:

  • id uuid primary key (stable, AR-owned).
  • slug globally unique and immutable (a DB trigger rejects updates).
  • (platform, entity_type, entity_id) unique: one namespace per owner, per platform.

Consumers needing org to namespace today: role assignments (#602144 (closed)), the AR UI, AR settings.

Problem

The earlier proposal had AR expose organization_id -> AR namespace ID/slug. That puts an Organization concept into AR's API and makes the monolith and UI depend on AR at runtime for a lookup outside AR's domain.

Decision

Persist the mapping in the monolith, populated from the provisioning response:

  1. Rails provisions via POST /api/internal/v1/namespaces with the owner anchor and slug. The call is idempotent on the owner anchor (platform, entity_type, entity_id); a lost-response retry returns the same namespace.
  2. AR returns the namespace, including its id (uuid).
  3. Rails stores (organization_id, ar_namespace_id) with unique(organization_id).
  4. Rails resolves the slug and status by UUID via GET /api/internal/v1/namespaces/:uuid and caches them. Only the UUID is persisted.
sequenceDiagram
    participant Rails
    participant AR as Artifact Registry
    Rails->>AR: POST /api/internal/v1/namespaces { slug, entity_type, entity_id }
    AR-->>Rails: 201 { id (uuid), slug, ... }
    Rails->>Rails: persist (organization_id, ar_namespace_id)
    Note over Rails,AR: later, on demand
    Rails->>AR: GET /api/internal/v1/namespaces/:uuid
    AR-->>Rails: 200 { slug, status, ... }
    Rails->>Rails: cache slug + status

Thereafter the monolith holds the namespace id and passes it on every applicable call. AR's API speaks only AR namespace ids.

Properties

  • Idempotent provisioning: the POST is idempotent on (platform, entity_type, entity_id), already a unique index in AR. A lost-response retry returns the same namespace, so activation is retry-safe. The Rails-side unique(organization_id) is the complementary guard. No idempotency-key header is needed.
  • Merge-safe, no AR change: if organization A absorbs B, B's namespace keeps entity_id = B, so it does not collide under AR's unique index. "A has two namespaces" lives only in the Rails table (two rows, same organization_id). One-to-many later is a Rails-side relaxation (drop unique(organization_id)); AR needs no migration.
  • Cached slug and status, safe when stale: both change only on AR's authority (the slug through the emergency escape hatch, the status through service conditions; ADR-007). AR enforces the status on every request, so a stale cached status only affects display until the next refresh. A stale slug can leave broken links until the next refresh; slug changes are rare emergency events.

Scope

In: the mapping table, populating it at provisioning, routing AR calls by ar_namespace_id, resolving and caching slug and status by UUID. Out: teaching IAM the data hierarchy (separate data-sync ADR); the legal slug-transfer flow.

Open questions

  1. Cache the slug, or resolve on demand? Resolved: cache slug and status (see Properties); persist only the UUID.
  2. Where in the activation flow (#593818) does the write happen, and what is the rollback if AR succeeds but Rails fails? (idempotent retry mitigates)
  3. Idempotency implicit on (platform, entity_type, entity_id), or an explicit idempotency-key header? Resolved: implicit on the owner anchor (ADR-009).

References

Edited by 🤖 GitLab Bot 🤖