D122 — Conduit code removal: legacy adapter, SDK, and webhook source deleted; enum case retained
Architecture decision record. Status, thematic clusters, and how to record a new ADR: the decision log index. Design of record:
docs/superpowers/plans/ 2026-09-05-conduit-code-removal.md. Follows D118 (Bridge registered), D120 (Bridge the sole granted primary, Conduit in no grant), and D121 (the coverage programme). Stacked onfeat/bridge-coverage-programme.
Context
Since D118/D120, Conduit has been legacy code only: no grant, no permitted key on any group, not
the code-level default (config('rails.default_provider') = 'bridge'), and — since D120's arch
guard — zero domain-module literal references. The app carries no production data at removal
time (no live tenants, no in-flight Conduit-routed state to migrate or drain). With Bridge
serving every group Conduit used to serve, and Sumsub/Utila/Fingerprint/SEON still registered
(parked or granted for Fraud), the Conduit adapter, its SDK, and its webhook source were dead
weight: a full sealed adapter (17 files), a 183-file vendor SDK package, 12 webhook handlers, and
their pinned tests, none of it reachable by any routed capability. This ADR records what was
removed, what was deliberately kept, and which features die with the vendor rather than surviving
as a "coverage gap."
Decisions
- Deleted the Conduit rails adapter (
app-modules/rails/src/Conduit/**, 17 files): the SDK connector wiring, DTO mappers,ConduitExceptionTranslator,providers.conduitconfig entry,sensitive_fieldsscrubber rows,.env.exampleCONDUIT_*vars, and the rails/webhooks composer requires onstables/conduit-sdk(commit6948ef82, D122 c3). - Deleted the Conduit webhook source (commit
e4ad78f4, D122 c2): 15 handlers (the plan's 12 plusWalletSignerUpdatedHandlerand the conduit-scopedCustomerCreated/UpdatedHandlerarms), theconduitwebhook-clientconfig entry + route,ConduitSignatureValidator+SignsConduitWebhooks+ theWebhookSignatureVerifierbind, andenvelopeFor()/updatedAt()off the three abstract webhook-handler bases.ProcessInboundWebhookJob'sDELIVERY_ID_HEADERwas generalised from a hardcodedx-conduit-delivery-idconstant to a per-sourcedelivery_id_headerconfig value (Bridge ships none — see "features retired" below). - Deleted the
RailsProviderumbrella interface (app-modules/rails/src/Contracts/ RailsProvider.php, commite4ad78f4): Conduit was its only implementer; the 13-capability composition it existed to express has no consumer left.ReturnTypeTest's umbrella allow-list entry and the umbrella container bind (already deleted in D120) are both gone. - Deleted
CustodyAccountResolver's Conduit arm, renaming$conduitCustomerExternalId→$customerExternalId(commit6948ef82) — the resolver is now Utila/Bridge-only. - Deleted the legacy infer-the-source payout body in
PayoutSubmitter(commit6948ef82): once no surviving adapter infers a transfer source, the no-nomination code path became dead code reachable only by tests; it now throwsProviderValidationExceptionpre-wire, with a regression test proving the refusal.PinsLegacyConduitRouting(the test helper that pinned Conduit as "the provider that infers a source") is deleted with it. - Deleted
webhooks:event-types(D68's diagnostic command, commit6948ef82): it diffed Conduit's live event vocabulary against the registry; with no Conduit source there is no vocabulary left to diff. - Deleted the two
ModuleBoundaryTestStables\ConduitSDK-confinement rows and theProviderLiteralTestexemptions 1 (inbound-webhook envelope default) and 2 (the'conduit'webhook-source-key idiom) — both exemptions existed only to admit a Conduit-specific literal that no longer compiles once the SDK and webhook source are gone. The raw-string tripwire (matching bare'conduit'/"conduit") stays live for any future reintroduction. Landed as c4, commitsb065ba2d(SDK + composer wiring deleted,ModuleBoundaryTestrows removed) andd640de54($providerCoderename, fraud fallback, webhook source fixtures) — both before this ADR's own commit. - Skill renamed
conduit-mirror-domains→mirror-domains(commitsfe8fc310,9507f99b, D122 c5): source directory,SKILL.mdname:+ provider-neutral wording,boost.json, the skills README row, the 9 sibling-skill cross-references, two wiki pages, and three tracking docs; stale generated mirror dirs removed withgit rm -r(boost never deletes on rename); mirrors regenerated withmake boost-update. - Deleted the emitter-less vendor co-sign chain (D122 F-code round, commit
05c9cea6): once c2 (e4ad78f4) deleted the Conduit webhook handler that dispatched it, funding'sEVENT_AWAITING_USER_SIGNATUREsignal had no emitter left — funding's match arm,emitAwaitingUserSignature(), and thecoSignContextpayload; theTransactionAwaitingUserSignatureevent, theRecordPayoutSignatureContextlistener, and theRecordPayoutSignatureContextCommand/Handlerit dispatched (five files) are all gone. Verified emitter-less with Serenafind_referencing_symbolson both the event and the command before deletion. Dual control is the in-app custody-controls approval-quorum engine (D97) — the vendor-hosted co-sign is retired with no replacement needed. Blemish, accepted, not rewritten: these five file deletions actually landed in0d727f15(a commit titled as a docs change) rather than in this F-code commit as their own message implies —0d727f15's diff carries the deletions;05c9cea6's message documents the reasoning. History is not rewritten for this; noted here for anyone reconciling commit messages against diffs.
Kept — live degradation seams, not dead code
FinanceProvider::Conduit(enum case) — kept, docblocked "legacy, no adapter."IsProviderMirrorenum-casts every mirror'sprovidercolumn; deleting the case makes any residualprovider='conduit'row (staging fixtures, historical test data) unhydratable at read time. Deleting it is a human-approval surface (a destructive narrowingCHECKmigration across every mirror table) for a purely cosmetic benefit — deferred (see Follow-ups).ManagesSignersAndQuorumNatively+ its 409 gate — the native-signer-management refusal Conduit's ceremony-less ISP redesign (D96) introduced stays live: it is the gate any FUTURE adapter without native signer/quorum support (Bridge included) hits, not Conduit-specific code.ManagesDocuments+ its guards — no surviving adapter implements it (Bridge collects documents via its own hosted flow, not this interface), but the job (ForwardDocumentToProviderJob) and the guard inSubmitOnboardingApplicationHandlerstay: the degradation seam (a doc-forwarding capability that legitimately has zero servers today) is the live behaviour under test, not a Conduit-only code path.- The three abstract webhook bases (
Abstract{Accounts,Funding,Payments}WebhookHandler) — survive; Bridge's own handlers extend them for shared parse helpers. Onboarding's abstract base needed no change. PayoutForwarderandVerifiesIdentity/InitiateVerificationCommand— both provider-neutral; an early explorer pass flagged them as Conduit-only and was refuted on inspection (Bridge and the parked adapters use the same seam).- The held-deposit/RFI domain events — kept as live non-handler callers
(
DepositAwaitingSenderInformation,DepositHeldForSenderInfoTimeout); see "features retired" below for what specifically dies alongside the Conduit handlers that used to fire them. (The awaiting-user-signature co-sign chain is NOT in this list — see Decision 9: it was deleted whole, emitter-less, once c2 removed the Conduit webhook handler that dispatched it.) - The payout co-sign OBSERVE columns (
verification_url/required_approvals/signature_expires_atonpayouts, plus theirPayoutData/PayoutResource/PayoutMirrorfields) — survive, now NEVER written by any code path (Decision 9 deleted the only writer). A code sibling (payments) marks the mirror mapping@deprecated. Dropping the columns is a migration, not a code deletion — tracked as a Follow-up. - Five now-dispatch-less accounts mirror/freeze commands — the account-freeze/mirror commands that only Conduit's webhook handlers dispatched are kept as-is; retiring them is a mirror-domain retirement exercise of its own, not this removal (see Follow-ups).
WebhookEventRegistry::registeredTypes()— kept; it is source-agnostic infrastructure the registry needs regardless of which sources are registered.
Features retired WITH the vendor (not coverage loss)
Some Conduit-era test coverage disappears because the feature itself was Conduit-specific, not because a Bridge-equivalent test was skipped. Recorded honestly here rather than folded into the coverage-equivalence gate as an unexplained gap:
- Conduit inbound-deposit RFI/held-deposit timeout handling — Bridge has no inbound RFI concept; the domain events/listeners/columns survive (see Kept) but the Conduit-specific triggering handlers are gone.
- The vendor payout co-sign webhook — Conduit hosted a provider-side co-sign ceremony over a webhook; the app's own in-app approval-quorum engine (D97) is the dual-control mechanism for every current and future adapter, so this retires cleanly with no replacement needed.
- Native signer/quorum management (the wire-ceremony methods in
SignerQuorumManagementTest, 11 methods) — the 409 gate that refuses it for a marker-less provider stays (see Kept above); the ceremony-specific wire tests die because no adapter still speaks that wire. - The "infer-the-source" legacy payout/order body — Conduit was the only adapter that
inferred a transfer source; every surviving/future adapter requires an explicit nomination (the
RequiresExplicitTransferSourcemarker, D120).PayoutSubmitter's dual-body branch collapses to one shape. - The
webhooks:event-typescommand — diagnosed Conduit's own runtime event vocabulary; no analogue needed since every surviving source's vocabulary is config-fixed, not runtime-discovered. - Document forwarding (the wire-level forward tests) — no adapter implements
ManagesDocumentspost-removal (none did post-D120 either); the reachable behaviour is the docless no-op path, which keeps its own test. - Registered addresses have no serving adapter —
ManagesRegisteredAddressesis stub-only on Bridge (crypto destinations are inline per-transfer, D118) and unimplemented by every other active adapter; the domain command stays (Sovera may serve it later) and keeps today's post-D120 posture — a routed Bridge call returnsPROVIDER_OPERATION_NOT_SUPPORTED(422).
Neutral fixtures introduced
ContrastProviderAdapter(tests/Support/Fixtures, rails c3 prerequisite, commit4e3e5e4e) — a registered-but-not-realProviderAdapterfixture carrying the classification arms Conduit was the last live source of (ManagesDocuments,ManagesSignersAndQuorumNatively,ManagesPayoutswithoutRequiresExplicitTransferSource); every method throws, since the seams under test inspect the class-string/resolved type, never the wire.- Parked adapters standing in for capability-missing fixtures — where a test needed "a
provider that does NOT serve capability X," Sumsub or Utila (still registered, parked) replace
Conduit as the negative fixture (e.g.
BankLinkTest's capability-missing arm → Utila). AllowsStrayVendorRequests— the Integration-suite trait letting class-based Integration tests opt out of the stray-Saloon-request guard (Pest'suses()hooks never reach class-based tests), introduced during this stack to keep the D121-era stray-request posture intact through the removal.
Production defect fixed en route
BridgeRailsProvider::submitApplication() matched the payload's top-level type key against
ApplicationType, while every submit/re-submit caller (SubmitOnboardingApplicationHandler,
SubmitRfiResponseHandler) threads the declared CustomerType subject into that same key — a
real Bridge submission would 422 PROVIDER_OPERATION_NOT_SUPPORTED for any business or individual
application. Existing tests stubbed OnboardsCustomers/the concrete adapter and never exercised
the real mapping, so nothing caught it before onboarding's c1 re-targeting pass hit the real
adapter. Fixed in commit 8d7f3eb5 (submitKycLink($payload, $subjectType), failing-first via
revert) — a pre-existing defect since D118, not introduced by this removal, and flagged
cherry-pickable to main ahead of the stack. Cross-reference: this is an erratum on
D118, not a D122 decision in its own right.
Test rule and coverage-equivalence gate
Tests were triaged by SUBJECT: an assertion about Conduit's own wire shape (SDK request/response,
mapper output, idempotency body, exception translation, signature bytes) was deleted; an assertion
about domain behaviour that merely used Conduit as its pinned vehicle was re-targeted to Bridge
(dropping the pin, MockClient on BridgeConnector); 'conduit' used as an arbitrary provider
string was swapped to 'bridge' (or, where a fixture needed a THIRD/capability-missing provider,
to a parked adapter). The coverage-equivalence gate (one row per deleted test method — deleted test :: covering Bridge test, no empty cell without a "features retired" justification) lives in
the PR body; the full working table is the removal-branch scratchpad
(prog-coverage.md), organized by commit (c2/c3) and module — that table is where the per-commit
test-METHOD deltas (e.g. c2 −85/+11, c3 −141/+9) live; they are coverage bookkeeping, not additive
to the run counts below (a deleted/added method count does not reconcile 1:1 against a passing-test
delta once datasets/providers are in play). Measured passing-test totals, each from a real
make test-parallel run at that commit: 3288 at the tip of PR #136/feat/bridge-coverage-programme
(3ab15783) → 3211 after c2 (e4ad78f4) → 3081 after c3 (6948ef82) → 3086 at c4/HEAD
(commits b065ba2d…98c600fb), confirmed by two independent full-suite runs — see Verification.
The delivery_id_header per-source generalisation
ProcessInboundWebhookJob's per-attempt delivery-id Context key was a hardcoded
x-conduit-delivery-id constant (DELIVERY_ID_HEADER). Removing Conduit forced the
generalisation the multi-source design always implied: delivery_id_header is now a per-source
webhook-client config value, defaulting to null (no header recorded) for a source that ships
none — which is Bridge's actual shape (Bridge's retry delivery carries no separate per-attempt
delivery-id header the way Conduit's X-Conduit-Delivery-Id did).
Consequences
- Deploy-day is a no-op for routing. Nothing currently routes to Conduit (D120), so removing
its adapter changes no runtime behaviour for any live or future tenant. The
webhook-client-conduitnamed route is gone; any historicalwebhook_callsrows withsource = 'conduit'remain readable (the model doesn't enum-castsource) but nothing ingests new ones. - Staging rows are harmless. A
provider = 'conduit'mirror row from prior staging activity stays hydratable (FinanceProvider::Conduitkept) but can never be created again by any code path. - The vendor SDK no longer exists on disk —
packages/stables/conduit-sdkwas deleted in c4 (commitb065ba2d, landed before this ADR's own commit; see Verification).
Follow-ups
- Delete
FinanceProvider::Conduitwith the first real data-cleanup pass (a narrowingCHECKmigration across every mirrorprovidercolumn) — deferred until there is production data to justify the migration risk (human-approval surface per repo rule 3). - Retire the five now-dispatch-less accounts mirror/freeze commands as part of a dedicated mirror-domain retirement pass, not bundled into this removal.
- The D94 "literal cleanup" is now fully closed — D120 took the domain-module literal count to zero and added the arch guard; this removal deletes the two exemptions that guard used to admit, closing the loop entirely (no sanctioned Conduit-literal exemption remains).
- Sovera S1/S2 — the next committed vendor (FX/jurisdiction fallback where Bridge doesn't serve an activity type or jurisdiction) is unaffected by this removal; its own ADRs are sequenced separately.
rails.bridge.allowed_endorsementsallow-list — SHIPPED, not a gap any more. F-code round R7/R8 (commit36480279) addedconfig('rails.php')bridge.allowed_endorsements(rails.php: 221) andSubmitOnboardingApplicationHandler::assertEndorsementsAreOffered()enforces it BEFORE any document capture, vault write, or rails call — a client-requested endorsement code outside the list is a 422, fail-closed for a provider with no configured list. The default list is derived from the shop's own offering (one code per fiat currency already configured infiat_rails/virtual_account_routes— base, sepa, faster_payments, spei, pix + pix_onramp/pix_offramp, cop);cardsis deliberately excluded since the shop sells no cards product. A code sibling (G-code) is now aligning the key-resolution side with the tenant-routed adapter (readingconfig("rails.{$routedProviderKey}.allowed_endorsements")rather than a hardcodedbridgekey) so a future non-Bridge Onboarding primary is not silently unguarded.- Compliance's
ConduitHandoffFileris the last Conduit-NAMED live class — out of this ADR's removal scope (it never touched Conduit's SDK or adapter; it is a compliance filing-reference generator that outlived the vendor by name only).app-modules/compliance/src/Integration/ ConduitHandoffFiler.php, bound inComplianceServiceProvider(~L45), persistsCONDUIT-HANDOFF--prefixed filing references — regulatory records, so the reference FORMAT cannot simply be rewritten retroactively. Needs (1) a rename of the class/binding to a vendor-neutral name and (2) a decision on the filing-reference format for new filings (keep theCONDUIT-HANDOFF-prefix for continuity with existing regulatory records vs. mint a new prefix going forward) — tracked here, not actioned by this ADR.
Verification
git log --oneline main..HEADat the time this ADR was authored: c1 (tests re-targeted, 8 commits) → the pre-existing-defect fix (8d7f3eb5) → c2 (629cbb04prerequisites +e4ad78f4app deletion +289d3fb3READMEs) → c5 skill rename (fe8fc310,9507f99b, landed concurrently with c2/c3 as a docs-only parallel track) → c3 (4e3e5e4eprerequisites +6948ef82rails deletion +619272b0READMEs).- c4 (SDK + root composer +
ModuleBoundaryTestrows +RailsException::$conduitCode→$providerCoderename) — LANDED, before this ADR's own commit:b065ba2d(deletespackages/stables/conduit-sdk, 183 files; the rootcomposer.json/composer.lockwiring; the twoModuleBoundaryTestStables\Conduitconfinement rows) andd640de54(theRailsException::$conduitCode→$providerCoderename, the fraud fallback, and the webhook source fixtures). Measured test totals across the full stack: 3086 passing at c4/HEAD, confirmed by two independentmake test-parallelruns (see the arithmetic in the Test rule and coverage-equivalence gate section above). make wiki-check— passes (see the PR body / commit trailer for the run).
Related
- D118 — the Bridge adapter this removal leaves as the sealed primary for the four groups Conduit used to serve.
- D120 — made Conduit legacy-only in the first place (no grant, no permitted key, not the default, zero domain literals); this ADR deletes the code that D120 made unreachable.
- D121 — the coverage programme this removal is stacked on;
its
ContrastProviderAdapter-adjacent fixtures and stray-request posture carry through. docs/tracking/multi-provider/00-provider-router.md— the live built-vs-deferred tracker, updated in this same change to reflect five adapters (bridge, sumsub, utila, fingerprint, seon) and the closed D94 literal-cleanup item.docs/archive/conduit/— the archived Conduit integration plan and multi-provider execution plan (moved, not deleted, in this same change).