14 KiB
Core Hub absorption and /api/v2 cutover plan
Status: accepted joint input for HUB-WP-0004-T06 and
CORE-WP-0010-T02
Date: 2026-08-21
Runtime source: hub-core
Rollback runtime: core-hub on railiance01
Decision
Absorb Core Hub into hub-core by capability and data boundary. There is no big-bang route move and no period with two production writers for the same boundary.
Hub-core owns the surviving wheel, runtime image, extension contracts,
compatibility routes, durable framework store, and migrations. Core Hub stays
deployable as the rollback runtime until every /api/v2 route group, durable
record family, consumer gate, and rollback exercise has passed. Its Python
package is never imported by hub-core.
The current production baseline is Core Hub chart 0.3.2 on railiance01,
backed by the core_hub PostgreSQL schema. Its source database on CoulombCore
is already frozen and is historical rollback evidence, not a participant in
this absorption.
Non-negotiable invariants
- A route group has exactly one production writer at any moment.
- A write freeze, final export, count/content comparison, and explicit operator approval precede every production writer change.
- Shadow traffic consists of reads and synthetic fixture writes only. Do not mirror normal writes to both runtimes.
- Hub-core and Core Hub keep separate SQLAlchemy metadata and migration
lineages. Hub-core may copy legacy rows but must not import
core_hubmodels or migrations. - IDs, timestamps, relationships, source revision, source table, and import bundle hash survive migration as provenance.
- Only API-key hashes and prefixes move. Raw keys, static operator tokens, database credentials, and secret values never enter bundles or evidence.
- Authorization fails closed when its dependency is unavailable. The compatibility API-key validator is temporary; authorization decisions remain owned through the identity/policy boundary.
- Core Hub remains a tested rollback target until the stabilization and rollback-retirement gates close.
Runtime and database shape
The hub-core image selected by ADR-0001 is the candidate runtime. Its current
in-memory PortStore is not a migration target. The first absorption slice
must add a PostgreSQL implementation, production readiness checks, an audit
ledger, and migrations owned entirely by hub-core.
Use the existing platform PostgreSQL allocation currently assigned to Core
Hub; do not consume a fifth application slot. Add a distinct target schema
(provisionally hub_runtime) beside retained legacy schema core_hub, then
transfer the consumer declaration and dynamic credential identity as part of
the final package transition. Separate schemas make source/target counts and
rollback observable without mixing ORM metadata.
The deployment package currently named rapp-core-hub owns the verified
railiance01 chart, policies, secret projections, live verifier, and rollback
surface. During dual-run it must deploy both immutable image revisions or a
candidate release beside the legacy release. Before Core Hub is archived, the
package must either be renamed/replaced by a hub-core-owned rapp package or
recorded explicitly as the surviving package with updated ownership and
intent. No application source moves into the deployment package.
Traffic controls
Implement route-group controls before moving data:
HUB_CORE_V2_GROUPS: compatibility groups served by hub-core;HUB_CORE_V2_WRITE_GROUPS: subset allowed to mutate hub-core authority;CORE_HUB_V2_WRITE_GROUPS: legacy groups still allowed to mutate Core Hub;- an ingress/gateway route map selecting
core-huborhub-coreper group; - a shadow comparator that calls both read targets, normalizes ordering and volatile fields, and records hashes without response bodies containing sensitive data.
The exact configuration carrier may be Helm values rather than environment variables, but the three states are mandatory and independently observable:
| State | Public owner | Candidate behavior | Write rule |
|---|---|---|---|
| legacy | Core Hub | health plus synthetic fixtures | Core Hub only |
| shadow | Core Hub | normalized read comparison | Core Hub only |
| candidate | hub-core | public read/write for the group | hub-core only |
Direct service access is network-policy restricted. The route map is the normal traffic authority; application write-group checks are defense in depth. A configuration assertion must reject overlap between legacy and candidate write sets.
Route and module move order
Each slice is independently buildable, reversible, and evidenced. Later slices do not excuse a failed earlier gate.
S0 — Durable runtime, compatibility shell, and migration tooling
No public route moves.
- Add hub-core PostgreSQL
PortStore, session lifecycle, readiness, audit ledger, import/export commands, and a migration bundle schema. - Add
/api/v2router composition, legacy response serializers, bearer-token dependency, per-group write controls, and normalized shadow comparison. - Import the static operator token through the existing OpenBao/ExternalSecret lane and copy API-key hashes into the isolated compatibility auth store.
- Preserve current error/status semantics and unprefixed OpenAPI aliases.
- Extend the conformance suite with durable restart/rebuild, authorization allow/deny/unavailable, metadata isolation, and migration idempotence.
Gate: frozen source fixture imports twice without duplicates; all seven source table counts can be represented; production readiness fails when PostgreSQL or authorization is unavailable; no traffic has moved.
S1 — System and read-only discovery
Routes:
/healthz,/readyz;/api/v2/widget-types,/api/v2/event-types;/api/v2/annotation-categories,/api/v2/policy-scopes;/api/v2/openapi.json,/api/v2/openapi.yaml,/api/v2/docs;- compatibility aliases published by the current OpenAPI surface.
Move the catalogs, schemas, OpenAPI snapshot/export behavior, and public consumer fixtures. Static catalog responses are compared as canonical JSON. Readiness must reflect the dependencies required by enabled groups rather than reporting healthy from process liveness alone.
Gate: public catalog and documentation smoke passes against both targets; normalized payload hashes match; ops-hub public gate and activity-core resolver pass; rollback route-map change is rehearsed.
S2 — Registry and manifest authority
Routes:
GET/POST /api/v2/hubs;GET/POST /api/v2/hub-capability-manifests;PATCH /api/v2/hub-capability-manifests/{manifest_id};POST /api/v2/hub-capability-manifests/{manifest_id}/activate;GET /api/v2/hub-registry.
Modules/data:
- copy
hubsandhub_capability_manifestsinto hub-core registry authority; - preserve legacy UUIDs and camel-case compatibility serialization;
- validate new writes against
helixforge.hub-extension, while a named legacy adapter reads migrated 0.x records without silently rewriting them; - add activation audit records and expose registry projections with provenance.
Gate: table counts and order-independent row hashes match; manifest activation is idempotent and audited; protected allow/deny checks pass; ops-hub bootstrap can resolve and update its manifest; rollback delta replay is proven.
S3 — API consumers and compatibility credentials
Routes:
GET/POST /api/v2/api-consumers;POST /api/v2/api-consumers/{consumer_id}/api-keys;POST /api/v2/token.
Modules/data:
- copy
api_consumersandapi_keys, including status, scopes, quotas, hash, and prefix but never raw key material; - preserve one-time key issuance semantics and ensure generated secrets are absent from logs, projections, migration bundles, and evidence;
- keep the static operator credential and dynamic consumer keys in distinct custody lanes;
- record a residual owner and follow-on task for replacing compatibility keys with the platform identity/authorization ports. The shim may outlive Core Hub briefly, but it is not a permanent hub-core authority.
Gate: existing ops-hub and activity-core credentials authenticate against the candidate; invalid, revoked, missing, and dependency-unavailable cases deny; rate/quota behavior is either preserved or explicitly residual-owned; reverse delta replay for newly issued key hashes is rehearsed without exposing a key.
S4 — Widgets and interaction evidence
Routes:
GET/POST /api/v2/widgets;GET/POST /api/v2/interaction-events.
Modules/data:
- copy
widgetsinto the operator-surface registry/projection boundary; - copy
interaction_eventsinto the physically separate interaction-event authority while retaining legacywidgetId, event type, context, metadata, IDs, and timestamps in the compatibility view; - route new writes through
port.events.interactionand build/api/v2reads from the same authority rather than a second compatibility-only store.
Gate: counts and hashes match; projection rebuild fixture and migrated sample produce provenance-equivalent views; progress events never appear in the interaction family; ops-hub and activity-core each post and read back unique correlated evidence; rollback delta replay is proven.
S5 — Empty compatibility collections and operator console
Routes:
GET/POST /api/v2/annotations;GET/POST /api/v2/requirement-candidates;GET/POST /api/v2/decision-records;GET/POST /api/v2/deployment-records;GET/POST /api/v2/outcome-signals;/console.
The five collections have no Core Hub durable models and currently return
empty data. Preserve that behavior only as a compatibility adapter; do not
claim or create authority. Each endpoint needs an explicit retirement or
future-owner record before the adapter is removed. Rebuild the console from
hub-core projections and public APIs; it must not query the retained
core_hub schema.
Gate: exact empty/error compatibility passes, the console authorization boundary and visual smoke pass, and every deferred collection has a residual disposition.
S6 — Whole-host cutover and Core Hub retirement
After S1–S5 are in candidate state:
- Freeze all remaining Core Hub writes and capture a final encrypted backup, table counts, content hashes, schema revision, image digest, and correlation ID.
- Import any remaining deltas and require zero unexplained count/hash drift.
- Run hub-core conformance, the Core Hub deployed smoke, ops-hub bootstrap, and activity-core resolver/write-readback against the candidate image.
- With explicit operator approval, point
hub.coulomb.socialentirely at hub-core and set every Core Hub write group empty. - Observe at least seven consecutive days with no unexplained 5xx, no failed consumer gate, no data drift, and no normal request to Core Hub. Record any exception with an owner and expiry.
- Exercise rollback once during rehearsal. After the stabilization window and explicit retirement approval, scale Core Hub to zero while retaining its immutable image, database backup, deployment revision, and route-map rollback instructions until the agreed expiry.
- Move the final OpenAPI/contracts/smoke fixtures into hub-core history,
record residual owners, archive the
core-hubrepository read-only, and update the deployment package identity.
Evidence required for every durable slice
| Evidence | Pass condition |
|---|---|
| Source identity | Core Hub image/source revision and schema revision recorded |
| Target identity | hub-core wheel/image version, revision, and contract version recorded |
| Backup | Encrypted location and checksum recorded; no secret value logged |
| Counts | Per-table source/import/target counts with zero unexplained delta |
| Content | Order-independent canonical row hashes match after documented transforms |
| Provenance | Source table/ID/revision, import bundle hash, and target record IDs queryable |
| Contract | Static OpenAPI diff has no unexplained removed route/status/schema behavior |
| Authorization | Missing/invalid/revoked deny; allowed roles pass; dependency outage denies |
| Failure mode | Database, policy/auth, and candidate unavailability behavior recorded |
| Consumers | Owned ops-hub and activity-core gates pass against the candidate |
| Rollback | Reverse delta export/import and route-map reversal exercised |
An unexplained row, response, authorization, or provenance mismatch fails the slice. A waiver requires a human progress note and a live residual task.
Smoke and change ownership
| Surface | Owner during absorption |
|---|---|
| Hub-core unit, contract, conformance, migration, and image tests | hub-core |
| Legacy OpenAPI and deployed-smoke oracle | core-hub until archive, then historical fixture in hub-core |
| ops-hub public/bootstrap gate | ops-hub |
| activity-core resolver and interaction write/readback | activity-core |
| Helm render, policy, secret projection, live verification, traffic map, rollback | rapp package (currently rapp-core-hub) + platform owners |
| Production writer flip, rollback, scale-to-zero, archive | operator approval required |
Rollback procedure
For a failed candidate slice:
- Stop candidate writes for the affected group; do not enable legacy writes yet.
- Export the candidate-only delta with IDs and hashes, validate it against the reverse adapter, and import it into the retained legacy schema.
- Compare counts/content and run the legacy group smoke.
- Route the group to Core Hub, enable only its legacy writer, and confirm the candidate writer remains disabled.
- Record the failure correlation ID, data interval, revisions, evidence, and next owner. Do not continue to the next slice.
If candidate data cannot be replayed safely, keep both writers disabled and invoke the operator recovery path. Availability pressure does not authorize discarding acknowledged writes or enabling concurrent writers.
Completion criteria
HUB-WP-0004-T06 is complete when this plan is linked to CORE-WP-0010 and its
handoff is recorded. HUB-WP-0004 may then finish; it does not claim production
absorption.
CORE-WP-0010 completes only when all slices have evidence, production is on
hub-core, the stabilization and rollback gates close, residuals have live
owners, the deployment package names the surviving runtime, and Core Hub is
archived read-only.