Upbeat Data Bridge: The Full Entity Map (Both Strokes)
Extends the drivers arc to its logical end-state: everything in Upbeat finds a home in the Agend suite, flowing down (Upbeat → Agend) via source drivers and up (Agend → Upbeat) via the write-back outbox — so the Agend product suite acts as the data bridge, and spokes (Agend Pro WordPress, portal, mobile, client-built tools) never touch Upbeat at all. Sources:
.docs/reports/upbeat-domain-map.md(endpoint-level inventory, §1.1–1.10 + driver phasing §4),writeback-two-way-sync.md(up-stroke machinery, tiers W1–W4), ADR-001 (custom objects for the long tail). Status: PROPOSED MAP · 2026-07-07
1 · The bridge model
DOWN STROKE UP STROKE
Upbeat ──[source drivers: Singer taps,──▶ Agend suite ──[transactional outbox,
watermark + content-hash] (Supabase hub) reverse-map, echo guards]──▶ Upbeat
SPOKES (never touch Upbeat): Agend Pro WP · portal · mobile · client tools · widgets
▲ read via /v1 API ▼ write via /v1 API (hub-first, async propagate)
Every entity gets a direction class, which decides what machinery it needs:
| Class | Down | Up | Machinery |
|---|---|---|---|
| Mirror (ref data) | ✅ | — | driver only; Upbeat-owned, read-only in Agend |
| Ledger (append-mostly) | ✅ | append ops | driver + outbox creates (no update conflicts) |
| Shared entity | ✅ | ✅ | driver + outbox + field-ownership + conflict queue |
| Agend-native | — | — | no Upbeat leg (Loop threads, LMS content, custom objects) |
| Migrate-candidate | one-off import | optional legacy feed | adopt the native module; Upbeat leg retired |
| Oracle (request-time compute) | — | — | synchronous hub→Upbeat call at request time; neither stroke — a question, not a record |
The Oracle class, spelled out (asked directly 2026-07-08): the outbox governs mastered writes; it does not forbid synchronous calls where Upbeat is the computation engine. Canonical example: cart pricing — the legacy build creates a draft invoice per cart (Session::create_new_invoice → add_product_to_invoice) so Upbeat returns the real member-specific price, and checkout totals are the CRM's. That flow survives intact; what changes is who makes the call: the hub's connector (same OAuth/token machinery as the drivers), never the client's WordPress. Natural API home: the gateway cart's checkout/simulate route. Required hygiene (the async-proposal lesson): timeout + "price confirmed at checkout" fallback, quote caching in the hub, and a shrink path — price groups/price points already sync down, so each pricing rule mirrored into the hub removes a synchronous dependency. Strangler pattern, applied to computation.
2 · The map — every Upbeat domain, its Agend home, both strokes
WP column = what the legacy WordPress build imports this into today (the thing the bridge replaces).
| # | Upbeat domain (entities) | Agend home | Down stroke | Up stroke | Replaces in WP |
|---|---|---|---|---|---|
| 1 | Members (MemberDetail, referrals, login) | CRM contacts | ✅ shipped (upbeat-contacts) | W1 update · W3 create/password | WP Users + kiosk member sync |
| 2 | Membership catalogue (MembershipType/Grade/Plan, upgrade grades, regrade products) | CRM tiers + memberships | small ref driver (map §4 Phase 3) | W3 (upgrade/renewal requests) | WooCommerce membership products |
| 3 | Organisations (Account, subsidiaries, emailDomains, OrganisationRole, org categories, AdminAccounts) | CRM companies + contact relationships (org roles map naturally onto crm_contact_relationships) | next major driver (map §4 Phase 4) | W3 account update, role CRUD | company-admin / child-accounts plugin logic |
| 4 | Invoices & payments (Invoice, payment lines, PaymentPlan, PaymentProfile, CashSaleAccount) | CRM sales / billing | ✅ shipped (upbeat-sales) | W2 (invoice groups, record payment) | WooCommerce orders + pay-invoice flow |
| 5 | Products & pricing (Product, PricePoint, PriceGroup, Coupon, PostageCost, QuickUrl) | Commerce/cart (+ events products) | driver TBD (products feed) | W2-adjacent (lines ride invoices) | WooCommerce products, coupons, shipping |
| 6 | Events cluster (Event, child events, EventPackage, EventRegistration, Registration, waitlist, Speaker/Sponsor profiles, EventRequest, EventSurvey, Location) | Events app (native) | DECISION: sync vs migrate — §4 | W4 or retired by migration | The Events Calendar (Tribe) + attendee forms + event products |
| 7 | Entitlements (Entitlement, EntitlementType) | Benefits/entitlements engine (crm_benefits + @agend/entitlements) | high-value driver — domain map ranks this Phase 2: the CRM→LMS access bridge | entitlement/update (W3) | financial-member-only gating |
| 8 | Comms preferences (Member_Subscriptions, dynamic subs* booleans) | CRM consent/preferences (feeds existing Mailchimp sync) | small driver | subscriptions update (pairs with W1) | GF preference forms |
| 9 | CPD & education (CPD_Points, CPD_Type, Course, CourseEnrolment, Assessment, Qualification, EducationHistory, Institution, Subject) | LMS (CPD ledger, enrolments, assessments) + CRM (quals on the contact) | driver (ledger class — append-heavy) | qual/education CRUD (Tier 1), CPD append (LMS-earned points flow UP: courses completed in Agend LMS must land in Upbeat's CPD ledger) | CPD displays, qualification lists |
| 10 | Committees & governance (Committee, CommitteeActivity, MemberCommitteeActivity) | Loop (committee/user integration sync already exists in apps/loop) + CRM relationships | extend existing loop sync into a proper driver | role changes (Tier 5) | committee pages |
| 11 | Service & engagement (Case, Annotation, Task, EmailReport) | CRM tasks/notes + service-request automation (.docs/specs/2026-07-04-service-request-automation) | on-demand (case detail) | create ops (Tier 5) — cases/notes raised in Agend flow up | contact forms → CRM |
| 12 | Loyalty (Loyalty, LoyaltyPoint) | Custom object — no native module; first candidate for the generic driver (O4) | via generic driver → object_records | — (display-only v1) | loyalty widget |
| 13 | Ref data (Region, IndustryType, Licensee, PriceGroup, Subject, Institution) | CRM/LMS ref tables or custom objects via generic driver sweep (O4) | Mirror class — one config-driven pass | — (Upbeat-owned) | ACF choice lists (legacy acf.php loads these into field options) |
| 14 | Member card / barcode | Portal (derived — membership status + barcode generation, not a synced entity) | derived from #1 | — | membership card/barcode widgets |
Coverage check: every object class in the legacy connector's inventory (~45 read families, ~45 write methods) lands in exactly one row above. The long tail (12, 13) needs no bespoke drivers if O4 (generic driver → custom objects) is built — that single capability closes the map.
3 · How "both ways" works per entity (the storage contract)
- One field-ownership registry per entity (extends the pattern shipped in
crm-loader.tsfor contacts): each field is Upbeat-owned (down only, read-only in Agend UIs), Agend-owned (up only, ingest never overwrites), or shared (both strokes + conflict queue per writeback doc §7). - Down stroke = the existing driver pattern unchanged: Azure-AD auth, paged fetch, watermark delta, content-hash skip, per-account Inngest orchestration. New entities are new drivers (or generic-driver configs), not new architecture.
- Up stroke = the writeback outbox unchanged: hub-first write, transactional enqueue, ordered operation groups, echo prevention via content-hash convergence. New entities are new
WriteBackOperationtypes +reverseMaprules, not new architecture. - The strangler contract for Agend Pro WP: per entity row, once BOTH strokes are live, the WP integration flips from
Iugo_Membership_Kiosk_APIto the Agend API — and that slice of the proprietary plugin retires. The bridge map is therefore also the plugin retirement schedule (and the code-protection sunset schedule). - Ledger-class nuance (row 9): CPD is the clearest case where Agend originates data — LMS course completions must flow UP into Upbeat's CPD ledger. Append-only ops avoid the conflict problem entirely; this is the easiest "up" class after profile updates.
4 · The events decision: sync vs migrate (O3)
The hardest driver (~40% bespoke per the pilot doc) vs a native module that already exists. Decide per this framework before W4 work starts:
| Criterion | Favours SYNC (build the driver) | Favours MIGRATE (adopt native Events) |
|---|---|---|
| Where does staff run events day-to-day? | Dynamics/Upbeat screens | Anywhere (they'd retrain to Agend) |
| Financial coupling | Event revenue must post through Upbeat invoicing regardless | Invoicing already bridged via rows 4–5; events revenue can ride it |
| Model fit | Upbeat's packages/price-groups are load-bearing | Native tickets/packages cover the real usage |
| Effort shape | Hardest driver + W4 write-back | One-off migration + staff change management |
| End-state | Two event systems forever, bridged | One event system; Upbeat events become history |
Recommendation to evaluate first: a hybrid — migrate event management to native Events, keep a thin financial up-stroke (registrations → Upbeat invoices via the already-planned W2 machinery). That deletes the events driver and W4's registration write-back, keeping only money flows Upbeat genuinely needs. Needs a client-workflow audit (who touches events in Dynamics today?) before committing.
5 · Sequencing (merging the domain map's phases with write-back tiers)
| Order | Slice | Why now |
|---|---|---|
| 1 | Entitlements down (row 7) | Domain map's own "highest value" call — unlocks access gating platform-wide |
| 2 | Orgs down (row 3) + membership catalogue (row 2) | Completes the CRM graph; prerequisite for W3 |
| 3 | W1/W2 up-strokes (already designed) | Contacts + invoices become truly bidirectional |
| 4 | CPD ledger both strokes (row 9) | Easiest up-class after W1; high member value |
| 5 | Generic driver → custom objects (O4) | Closes rows 12–13 and all future long tail as config |
| 6 | Events decision (§4), then execute chosen path | Deliberately last — the decision may delete the hardest work |
6 · Client configurations: the bridge is optional per client
Because the hub is the system of record natively, Upbeat is a configuration, not a dependency. The same suite (and the same thin WordPress plugin + widgets + API) serves three client types — only the back-of-hub differs:
| Configuration | Down stroke | Up stroke | Who |
|---|---|---|---|
| Bridged | Upbeat drivers (this map) | Write-back outbox (W1–W4) | Clients with a Dynamics investment worth preserving (AHRI et al.) |
| Migrated | One-off import (Groundhogg transformers in apps/crm/lib/migration, wp-migration-* migrations), then none | None — hub IS the record | Legacy Agend-AMS clients; parity check first: legacy-ams-parity-map.md |
| Native | — | — | New clients, no prior CRM |
Migrated and native clients skip this entire map: no drivers, no field ownership, no conflict queue, and every write capability is available immediately (W1–W4 exist only because of Upbeat). One WordPress build serves all three (opportunity O6, platform-opportunities.md).
7 · What this map is
It is the answer to "can everything in Upbeat live in Agend?" — yes: 12 domains have native homes, 2 ride custom objects, and one (events) has a decision to make that might be cheaper than anyone assumed. It is simultaneously the driver roadmap, the write-back roadmap, the WP plugin retirement schedule, and the sunset plan for code protection — one map, four programmes aligned. And per §6, it is the maximal case: only bridged clients need any of it.
What this map is not is a mandate to rebuild Upbeat's authoring surfaces — see strategic-posture-upbeat.md for the standing answer to "are we replicating Upbeat?" and the operating rule that governs every entity flip.