Skip to main content

ADR-020: Runtime Data Governance β€” Control Plane vs Git

StatusAccepted
Date2026-01-23
Decision MakersChristophe ABOULICAM
Related TicketsCAB-850, CAB-849, CAB-848

Context​

STOA Platform uses GitOps for infrastructure and configuration management. However, a critical question arises: where should API runtime metadata live?

Current State (Anti-Pattern)​

Developer modifies category in stoa-catalog/*.yaml β†’ Git push β†’ GitLab CI β†’ Deployment β†’ Runtime updated

Problems Identified​

IssueImpact
No business validationInvalid categories accepted
No audit logWho changed what, when, why?
No RBACAnyone with Git access can modify
No approval workflowChanges go live without review
No stakeholder notificationAPI owners not informed
Complex rollbackGit revert vs API call

Decision​

Split data governance by type:

Data TypeSource of TruthModification Method
OpenAPI SpecGit (stoa-catalog)PR + Code Review
Infrastructure ConfigGit (stoa-gitops)PR + Code Review
Runtime MetadataPostgreSQLControl Plane API

What is Runtime Metadata?​

Data that changes independently of the API contract:

  • category / tags β€” Classification
  • visibility β€” Community, AD groups
  • status β€” draft, published, deprecated
  • owner / team β€” Ownership assignment
  • sla / rate_limits β€” Custom policies

What Stays in Git?​

Data that defines the API contract:

  • OpenAPI specification (endpoints, schemas)
  • API name and base description
  • Version (major.minor.patch)
  • Protocol bindings (REST, GraphQL, gRPC)

Architecture​

Data Model​

-- Runtime metadata (editable via Control Plane)
CREATE TABLE api_metadata (
api_id UUID PRIMARY KEY REFERENCES apis(id),
category VARCHAR(100),
tags TEXT[],
status VARCHAR(20) DEFAULT 'draft',
visibility JSONB, -- {community_ids: [], ad_groups: []}
owner_team_id UUID REFERENCES teams(id),
custom_rate_limit INTEGER,
updated_at TIMESTAMPTZ DEFAULT NOW(),
updated_by UUID REFERENCES users(id)
);

-- Automatic audit log
CREATE TABLE api_metadata_audit (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
api_id UUID NOT NULL,
field_name VARCHAR(50) NOT NULL,
old_value JSONB,
new_value JSONB,
changed_by UUID REFERENCES users(id),
changed_at TIMESTAMPTZ DEFAULT NOW(),
reason TEXT
);

API Endpoints​

GET    /v1/apis/{api_id}/metadata       β€” Read metadata
PATCH /v1/apis/{api_id}/metadata β€” Update metadata (RBAC enforced)
GET /v1/apis/{api_id}/metadata/audit β€” View change history

Bootstrap Sync (Git β†’ DB)​

async def sync_catalog_to_db():
"""
Import initial metadata from Git.
Does NOT overwrite if already present in DB.
"""
for api_yaml in git_catalog.list_apis():
if not await db.api_metadata_exists(api_yaml.id):
await db.create_api_metadata(api_yaml)
# else: DB is source of truth, Git ignored for metadata

Consequences​

Positive​

  • Audit trail β€” Complete history of who changed what
  • RBAC β€” Only authorized users can modify metadata
  • Validation β€” Business rules enforced at API level
  • Notifications β€” Stakeholders informed of changes
  • Simple rollback β€” API call vs git revert

Negative​

  • Two sources β€” Spec in Git, metadata in DB
  • Sync complexity β€” Bootstrap logic needed
  • Migration β€” Existing YAML metadata must migrate to DB

Neutral​

  • Console UI required β€” Need UI for non-technical users
  • API versioning β€” Metadata API needs versioning strategy

Implementation​

See CAB-850 for implementation details.

Phases​

  1. Phase 1 β€” Data model + Alembic migration
  2. Phase 2 β€” API endpoints + RBAC
  3. Phase 3 β€” Console UI
  4. Phase 4 β€” Sync + migration script

References​