Skip to main content

Decision Record: Integration Registry ↔ IReferenceProvider Convergence

Status: Decided · Date: 2026-06-15 · Operationalizes: ADR-041 Decision #3 (the deferred open question) · Type: Investigation / spike — no production code changed.

ADR-041 §3 recommends that cross-app rendering of linked things SHOULD align with NC's native OCP\Collaboration\Reference\IReferenceProvider, while the OpenRegister integration registry keeps its bespoke layer only for the CRUD it genuinely adds beyond read-only references — and defers "whether and how it converges" to a dedicated investigation. This is that investigation.

Headline recommendation

KEEP-SEPARATE-BUT-ALIGN. Do not converge the OpenRegister integration registry (ADR-019 leaf system) onto IReferenceProvider. Keep the two mechanisms separate — each is the correct shape for its job — and align the seam between them.

  • Cross-app render-a-linkIReferenceProvider. OpenRegister already does this with OCA\OpenRegister\Reference\ObjectReferenceProvider (registered via registerReferenceProvider). Future "show app X's record as a rich card in app Y" SHOULD be a reference provider in the owning app, not a leaf in OR's registry.
  • Object-scoped CRUD-over-linked-things → OR integration registry. The write verbs (create/update/delete), the openregister_*_links tables, the (register, schema, objectId) scoping, and the OpenConnector transport are genuine added value with no IReferenceProvider equivalent. Bound it (ADR-041 §2); do not replace it.

Why (responsibilities matrix)

OR registry responsibilityBucketCovered by IReferenceProvider?
list(register,schema,objectId)READ, object-scopedNo — no "list the X of object Y" verb
get(...entityId)READ/RENDERPartial — returns full entity, not a preview card
create / update / deleteVALUE-ADDNo — reference providers are read-only
openregister_*_links tables + magic-columnVALUE-ADDNo — references are stateless
(register, schema, objectId) scopingVALUE-ADDNo — no object-graph binding exists
getStorageStrategy + ExternalIntegrationRouter (OpenConnector)VALUE-ADDNo — no transport abstraction
getId/getLabel/getIcon/getGroup discovery metadataREADYes → getId/getTitle/getIconUrl/getOrder
render a link to an OR object in another appREAD/RENDERYes — already done by ObjectReferenceProvider

The overlap is the narrow render-a-link edge, and it is already served by an IReferenceProvider. Everything that makes the registry distinct has no reference-provider analogue. Forcing the value-adding 90% (CRUD, link tables, scoping) through a read-only contract would be a semantic abuse analogous to the GetTaskProcessingProvidersEvent abuse ADR-041 rejects.

Contract comparison (the shape mismatch)

  • IReferenceProvider: matchReference / resolveReference → IReference + getCachePrefix / getCacheKey. Cross-app by design (registerReferenceProvider), read-only, first-class caching via IReferenceManager. IReference is a preview (title/description/image/url/ richObject), not a mutable entity.
  • OR IntegrationProvider: list/get/create/update/delete + metadata, (register,schema, objectId)-scoped, registered only from OR's own boot (no sibling-app contribution hook). Storage strategies observed across the 22 built-ins: link-table ×19, external ×4, query-time ×3, magic-column ×1. CRUD-capable leaves include Notes, Tasks, Contacts, Deck, Email, OpenProject, Xwiki, Files, Tags, Calendar, Shares.

One line: IReferenceProvider = "render a link to a thing identified by URL/text" (cross-app, cached, read-only); OR registry = "manage the things linked to this object" (object-scoped, CRUD, persisted links).

Migration blast radius (why full convergence is rejected)

DependentFull-convergenceKeep-separate-but-align
Manifest referenceType markers (single-entity widgets)BREAK — expect entity get(), not a previewTransparent
22 built-in providers (5 builtin + 17 providers)BREAK/rewrite — split read vs write per leafPreserve
Frontend useIntegrationRegistry / CnObjectSidebar / CnIntegrationCard / CnFormDialogBREAK — object-scoped CRUD has no reference modelTransparent
ADR-019 (registry pattern, Tier-2 parity, 3-stage filter)CONTRADICT accepted ADRPreserve + clarify
ADR-036 (universal widget manifest v2, single-entity surface)BREAKTransparent
openregister_*_links (×17) + migrationsORPHAN/migratePreserve

Full convergence is a fleet-wide breaking change for a benefit already delivered. Cost/benefit favors keep-separate-but-align decisively.

Phased follow-up (alignment — small, optional, additive)

  • Phase A (this record): declare the boundary — references for cross-app render, registry for object-scoped CRUD.
  • Phase B (future, additive): cross-app render needs (e.g. decidesk/docudesk cards in a sibling app) ship an IReferenceProvider in the owning app — not a leaf. No OR change.
  • Phase C (future, optional): a leaf's read path MAY delegate preview rendering to IReferenceManager::resolveReference() instead of bespoke preview code. Per-leaf, reversible.
  • Phase D (guard): a gate flagging any new leaf whose sole purpose is cross-app rendering (should be a reference provider), complementing the ADR-041 anti-RPC gate.

Risks

  • R1 Two-mechanisms confusion — mitigated by the one-line rule + Phase-D guard.
  • R2 Preview duplication drift if Phase C is skipped — cosmetic, low severity.
  • R3 Temptation to re-add cross-app contribution to the registry — explicitly forbidden by ADR-041 §2; reaffirmed here.

Read-only PoC (illustration only, not wired)

// ILLUSTRATIVE — not added to any boot path. A leaf read path could reuse NC's
// cached, cross-app reference machinery for previews without any registry change:
$ref = $this->referenceManager->resolveReference($url); // IReferenceManager
return $ref === null ? null : [
'title' => $ref->getTitle(),
'description' => $ref->getDescription(),
'imageUrl' => $ref->getImageUrl(),
'richObject' => $ref->getRichObject(),
];

Decision

KEEP-SEPARATE-BUT-ALIGN. The ADR-041 deferred open question is resolved: do not converge. IReferenceProvider is the cross-app render-a-link mechanism (already in use); the OR integration registry is the object-scoped CRUD-over-linked-things mechanism (bounded by ADR-041, not replaced). Align the seam via the optional additive phases above. No registry code is changed by this investigation.