WastePlace · Boston Marketplace Pilot

Wiring-Trace Explorer — every interaction row, demand → backend

All 484 interaction rows from the 20 wireframe audits in wiring-trace/, embedded verbatim — element, behavior, backend binding, verified exists-status against platform-develop/ (read-only), and v0 disposition mapped to the WIRING_GUIDE Day 1–5 plan. Companion to MASTER_INVENTORY.md (116 deduped backend surfaces + ranked open questions).

484 rows · 20 pages 116 deduped backend surfaces 37 consolidated open questions Audited 2026-06-10 · read-only
484
Total rows
59
YES — exists today
112
PARTIAL — substrate
282
NO — net-new
31
Chrome / static
484
Matching filters
Migration (Strangler) Entity model Bid vs Buy Rows Open questions (36 open · 1 resolved) Boston routing

Business decisions — decide before/at Day 0 (they shape code)

Recommendations are scoped to WIRING_GUIDE v0 (DR/JR instant booking; RS + leads deferred). Full sources + detail in MASTER_INVENTORY.md §(c).

Engineering decisions — decide before the day that builds them

Refactoring CTO review · Fowler / Feathers pattern vocabulary

Migration strategy — strangle the auction, don't overload it

We are not "adding a status to Jobs." We grow a new bounded context (Marketplace/) around the legacy Jobs monolith and route a thin slice of traffic (Boston + flag) into it, while the legacy auction runs byte-untouched. That is the Strangler Fig. Every other pattern below is a named mechanic that makes the fig safe. Full write-up: migration/MIGRATION_STRATEGY.md; the entity set it realizes is the Entity model tab and migration/ENTITY_MODEL.md.

Why this wins the skeptic: the team already refactors this way (verified read-only)

  • A Strategy seam with unimplemented stubsexecuteBookingStrategy (imports/api/Payment/server/booking.js:56-61) already has case 'instant' / case 'delta' literally throwing "not implemented yet". We fill the 'instant' case — we don't add an if.
  • A gated parallel-run of two Stripe implementationsprocessWithNewStripeImplementation vs processWithLegacyStripeImplementation (imports/api/Payment/methods/process.js:399 / :224), switched by a Settings flag at :698.
  • Three forked job-creation pipelines by sourcecreateJob (Job/methods/listing/helpers.js:128), createJobForNewUser (…/server/invertedFlow.js:42), createJobFromOscar (Oscar/server/quoteHandler.js:265).
  • An instant-book path that already bypasses biddingbookInstantRate (imports/api/InstantRate/services/server/booking.js:49) with its own durable-timer scheduler (InstantRate/services/futureTasks.js).

You don't have to believe the marketplace journey can be forked safely. The team has already forked job creation three ways, parallel-run two Stripe implementations behind a flag, and stubbed an 'instant' booking strategy waiting to be filled in. We are completing a refactor the codebase started.

1

Pattern → seam map — every pattern grounded in a real file

Strangler Fig
(umbrella)

New Marketplace/ bounded context grows beside Jobs; a router sends only Boston+flag traffic into it; the legacy auction is untouched.

SeamRouter/facade = Marketplace/helpers/server/bostonGate.ts (isBostonZip, isMarketplaceJob) gating on flag instant_marketplace (imports/core/Settings/index.js:217) + service zip. New context = imports/api/Marketplace/*.

Branch by
Abstraction

Add the buy booking behind the existing dispatch abstraction instead of scattering if (isMarketplace) through process.js.

SeamexecuteBookingStrategy({bookingType}) (imports/api/Payment/server/booking.js:49). The 'instant'/'delta' cases (:59-61) are stubs. process.js:511 already computes bookingType and calls the strategy at :570 — we implement a case.

Anti-Corruption
Layer

One translation module isolates the new context from the legacy Jobs shape; buy money/assignment never leak onto the Job.

SeamMarketplace/acl/server/legacyJobBridge.ts (new). Materializes a legacy Job in active shape with a synthesized winningBid at provider accept only; mirrors JOB_COMPLETED back. Translation table: ENTITY_MODEL.md §6b.

Parallel Run

Run both models against the same reality during the pilot; reconcile before contracting.

SeamDual-write at accept: Booking (record for money/assignment) + materialized Job (record for completion/invoicing), linked by Booking.legacyJobId. Nightly reconcile on the §4 invariants. Precedent: the gated Stripe parallel-run at process.js:698.

Expand–Contract
(parallel-change)

Add the new context without breaking legacy (expand); contract the coupling only after it proves out.

SeamExpand: new collections are additive; the legacy Jobs.status enum gains nothing (Job/helpers/status.js untouched — the buy machine is the Booking's own). Contract: default C1 = keep the ACL permanent; C2 (absorb completion) only if retiring the bid model.

Event
Interception

Buy events are a disjoint vocabulary; a booking can never emit a bid event into the auction.

SeamEventBus (imports/core/EventBus). Buy emits MARKETPLACE_* only; the ACL never emits JOB_POSTED (the fan-out discriminator from createJob). Reverse: the ACL listens for JOB_COMPLETED (from the unchanged jobs.complete, Job/methods/jobs.js:475) to mirror completion back.

2

The router (facade) — how traffic is split

The gate is evaluated at intake, re-checked on every draft mutation, and re-asserted inside the booking service — so no booking can exist outside the gate, and the router never silently re-routes.

incoming intake / checkout
bostonGate.isMarketplaceJob(zip)flag instant_marketplace (:217) AND isBostonZip(serviceZip)
BUY contextMarketplace/* · DraftQuote → Booking
◀ true | false ▶
BID flow (legacy)createJobJOB_POSTEDbids[]scheduleJob · BYTE-UNTOUCHED
flag OFF ⇒ facade short-circuits to BID everywhere — platform byte-identical  ·  KILL SWITCH
3

The ACL boundary — drawn explicitly

Only the fields the shared completion/invoicing/payout pipeline reads cross the boundary, and they cross once, in active shape. Everything money/assignment-shaped stays in the context.

DraftQuoteSHOPPING
pay 10%
BookingCONTRACTING
ladder
acceptProviderOffer
ACL
materializeJoblegacyJobBridge.ts
legacy Jobstatus:'active', winningBid:synth(payout), winnerBidID
▼   event interception (reverse)
Booking.status='completed'mirrored from event
intercept JOB_COMPLETEDEventBus
legacy jobs.completeUNCHANGED · jobs.js:475
✕ stays left, never written onto the Job:   payment{} (deposit/balance/refund)  ·  assignment{} ladder  ·  market  ·  bookingReference  ·  intake snapshot  ·  payout snapshot
4

Coexistence invariants — break one, isolation breaks

Each is enforced structurally (by construction or guard), not by convention.

  1. Born active, never open/booked-on-the-legacy-enum. The ACL writes the Job directly in active with a synthesized winningBid; it never calls scheduleJob, which hard-asserts status: STATUS.OPEN.value (Job/bidding/server/index.js:526-528, throws "not in open status"). The bidding matcher, every open publication, and JOB_POSTED fan-out can never see a buy job.
  2. One terminal pipeline. active → completed/cancelled is the same legacy code for both flows, hinged on the synthesized winningBid/winnerBidID (bid precedent: winnerBidID: this.userId, Job/methods/jobs.js:297). No forked completion, invoicing, chat-auth, or archival.
  3. The legacy enum gains nothing. The buy state machine lives on Booking.status, not Jobs.status. Job/helpers/status.js is untouched — the sharpest departure from the rejected "add 'booked'" plan.
  4. Buy money never lands on the Job. payment{} lives on the Booking; the legacy paymentType/invoice/PaymentsHistory bid path is byte-identical. stripe.refunds.create is only ever invoked against Booking.payment PIs.
  5. Disjoint sub-aggregates. A Job carries bids[]; a Booking carries assignment — different collections, never handed between flows. Legacy jobs.hauler.list keys on bids.audit.user (blind to buy pre-accept); the new provider publication keys on Booking.assignment.providerId.
  6. Disjoint event vocabularies, intercepted. Buy emits MARKETPLACE_*; the ACL guards a booking never emits JOB_POSTED. Bid events fire only from createBid/expireJobs, which buy code never reaches.
  7. Crons can't touch buy work. expireJobs queries status: {$in:['open','active']} and winningBid: {$exists:false} (System/server/cronjobs/jobs/expireJobs.js:65-66) — a buy Job is born active with a winningBid, so the second clause excludes it. Verified, no patch needed.
  8. Flag + zip gate, fail-loud. Buy entry requires the flag ON and a Boston service zip, re-asserted at materialization. Flag OFF = byte-identical legacy. Gate failure halts/diverts loudly, never silently re-routes.
  9. Idempotent money on one ledger. Every buy Stripe call carries a deterministic idempotency key off the shared correlationId base (Payment/methods/process.js:75-85); both flows log to PaymentsHistory. A re-fired timer cannot double-charge.
5

Phased cutover — and the risk each phase retires

Each phase is a flag-scope widening (team → pilot zips → broader), reversible by narrowing the flag. No big-bang switchover, no schema down-migration anywhere on the path.

P0 · Behind flag (dark)
FLAG OFF

Marketplace/ context, bostonGate facade, DraftQuote, Booking, ACL stub. Flag default OFF.

Retires: "does new code destabilize prod?" — flag OFF = byte-identical; staging deploy with flag OFF proves zero behavior change.

P1 · Money + ladder
FLAG ON · team accts

Implement 'instant' case in executeBookingStrategy; deposit/balance/refund PIs; webhooks; ladder on FutureTasks; ACL materialization at accept.

Retires: "does the new journey corrupt the shared pipeline?" — ACL writes only active-shape Jobs through the one seam; dogfood catches money bugs first.

P2 · Parallel run
FLAG ON · pilot zips

Flag ON for Boston pilot zips. Both models written at accept; nightly reconcile on the §4 invariants.

Retires: "do the two models agree on reality?" — reconcile surfaces drift (orphan bookings, Jobs missing winningBid, buy Jobs with bids[]) while the kill switch is one flip away.

P3 · Contract
FLAG ON · widen

After N clean reconcile-weeks: default C1 — keep the ACL permanent; the legacy Job is "the fulfillment record." C2 (absorb completion) only if retiring the bid model.

Retires: "are we carrying duplicate runtime mechanics?" — C1 retires it by design (completion never forked); C2 deferred.

6

Rollback / kill-switch story

  • Primary kill switch (already exists): Settings.isFeatureEnabled('instant_marketplace') (imports/core/Settings/index.js:217). Flip OFF → the facade short-circuits → every request takes the legacy bid path → the platform is byte-identical to today. No deploy, no migration, instant.
  • Blast-radius containment: only three existing-file edits ship, all flag-gated — (a) implement the 'instant' case in executeBookingStrategy, (b) add a taskType discriminator to the FutureTasks startup dispatch (System/server/futureTasks.js), (c) add buy webhook cases to the Stripe webhook switch. None change bid-path behavior; flag OFF, none run.
  • Data rollback is trivial because data is additive. Buy lives in new collections (DraftQuotes, Bookings) + additive-optional fields elsewhere. Disabling the pilot strands no legacy data and needs no down-migration. In-flight bookings drain to completion through their materialized Jobs (valid active jobs regardless of the flag).
  • Reconcile-before-contract: the parallel-run reconcile is the gate on Phase C. If reconciliation drifts, you stop contracting and keep the ACL — you never lose the "legacy Job is the record" fallback.