Skip to Content
Design DecisionsPortfolio Registry Services

Portfolio registry as three microservices on Cosmos-shaped Postgres

Status: Accepted · Date: 2026-07-02 (design v3), live in production 2026-07-03 · Area: Data

Context

Aurora needed a first-class registry of the portfolio’s Customer → Tenant → Solution hierarchy. Earlier design iterations placed it inside the webapp: v2 proposed an in-webapp module over a git-backed YAML registry repo. But the registry was becoming runtime infrastructure for more than Aurora — the tenant-scoped Orbit reader and the deploy write-back path both hit it — and a registry queried per-request needs real read/write latency, indexed filtering, and row-level concurrency, not Contents-API round-trips and sha-based commits.

Decision

  • Three standalone Hono microservicescustomer-service, tenant-service, solution-service — each a first-class platform component, sharing plumbing via implementation/shared/registry-kit.
  • Storage is Postgres shaped like Cosmos DB: one schema per service, one records table (id text PK, data jsonb, version int, timestamps), B-tree expression indexes per filterable JSON field plus a jsonb_path_ops GIN index. Documents are zod-validated at the service boundary and use camelCase keys (hyphenated style stays with human-edited YAML). The shape keeps a later lift to actual Cosmos mechanical.
  • Optimistic concurrency via If-Match: <version> (409 on stale), and every mutation appends to an audit.events table — keeping the git era’s auditability deliberately, not accidentally.
  • Invariants are cross-service, synchronous, and fail closed: unknown customer → 400, cross-customer attach → 409, delete-with-children → 409, peer unreachable → mutation 503 (reads never need a peer). No distributed transactions; a reconciliation sweep is the drift backstop. Solution ownership is a single tenant pointer on the solution record, so single-ownership holds by construction.
  • aurora-webapp is the BFF in front of the services (which run with internal ingress on Azure and hold no GitHub credentials): it joins registry records to solution.yaml by customer-repo name, exposes /api/customers|tenants|solutions, and serves the tenant-scoped reader’s contract.
  • Azure Postgres is Entra-only — no password exists anywhere. Services authenticate with a DefaultAzureCredential token as the pg password, refreshed per connection; the UAMI and the signed-in user are the Entra admins.

Consequences

  • The registry scales, deploys, and versions independently of Aurora, and GitHub App permissions stayed read-only — v2’s write-elevation blast-radius concern disappeared with the registry repo itself.
  • Schema-flexible documents mean field changes without column migrations; services run their own advisory-locked SQL migrations at boot.
  • Live-run costs were paid and recorded: AZURE_CLIENT_ID must be set or DefaultAzureCredential ignores the user-assigned identity; in-environment service URLs must use the app-name form (http://svc-customer) because the internal FQDN form does not resolve; and several az CLI renames had to be chased in the provisioner.
  • The 2026-07-03 production migration registered 7 customers, 7 tenants, and 7 solutions with a final sweep of 0 unregistered / 0 orphaned.

Evidence

  • implementation/customer-service/, implementation/tenant-service/, implementation/solution-service/, implementation/shared/registry-kit/
  • implementation/aurora-webapp/codebase/src/lib/registry/ — the BFF client, join, and write-back
  • infrastructure/azure/provision-postgres.sh, provision-services.sh, app-service.yaml.tmpl
  • designs/tenant-model.md (v3, incl. the v2 → v3 rationale table), memory/portfolio-registry.md