Skip to Content
Living documentation — last reviewed 2026-05-28
DecisionsADR-0015: Arbox parity mode ("Arbox Bridge")

ADR-0015: Arbox parity mode (“Arbox Bridge”)

Status: Proposed (design spike — no code yet; M1 validation complete, see 2026-07-29 addendum) Date: 2026-07-25 (amended 2026-07-26/27: Move/FreeFit middleman reframe; 2026-07-29: M1 results — see Addenda) Context owner: Saar Issue: FIT-258  (epic under FIT-65 , Linear project Move Integration, milestones M1–M5)

Like ADR-0011/0014 this is a design-spike ADR and deliberately exceeds the 200-line guideline: it is the canonical spec for the whole parity feature, not just one decision.

TL;DR — the decision

  1. Build parity mode as a one-directional-per-domain sync: Taikan → Arbox mirror for members, memberships and bookings; Arbox → Taikan poll-refresh for schedule and Arbox-side activity. Never two masters for the same field.
  2. Use the documented Arbox public v3 API (https://arboxserver.arboxapp.com/api/public/v3/*, header api-key) for all parity traffic. The v2 Business/Management APIs the importer uses stay import-only.
  3. Store the connection in integration_connections (new integration_provider value 'arbox'), not import_provider_configs.
  4. Add two new tables: arbox_entity_map (org-scoped id map, the single source of linkage and the echo-loop breaker) and arbox_sync_ops (outbox with per-aggregate FIFO, driving a new BullMQ arbox-mirror queue).
  5. Introduce the missing domain events (member.created/updated, booking.created/cancelled, subscription.created/cancelled/frozen/resumed) emitted post-commit at the existing service attachment points; an ArboxMirrorListener turns them into outbox ops. A nightly reconciliation sweep is the safety net for the (accepted) small gap between commit and listener.
  6. Billing has exactly one owner per member. Taikan-sold memberships are mirrored into Arbox as a client-created ₪0 non-recurring “Taikan” membership type. Payments are never mirrored, in either direction.
  7. Arbox stays schedule master during parity (the public v3 API cannot create classes — the staff-session API can; see the 2026-07-29 addendum). Taikan-created sessions are explicitly badged “Taikan-only”.
  8. Taikan never blocks on Arbox: mirror failures queue, retry with backoff, then park into a user-visible issues inbox. Circuit breaker pauses the org’s mirror on sustained failure.
  9. Rollout gated by PostHog flag arbox-parity-mode (org group, default OFF, unevaluable ⇒ OFF) plus platform-tier feature gate. (Retired: flag removed in #531; the bridge is no longer flag-gated.)
  10. A sandbox validation spike is a blocking prerequisite — the documented unknowns about v3 write semantics must be answered against a real box before Phase 1 code (FIT-259, now 14 questions). (2026-07-29: the read/propagation half is answered — see addendum; the write-semantics tail still needs a writable box.)
  11. (Amended 2026-07-26) The bridge serves two purposes: (a) the original migration parity, and (b) Move/FreeFit visibility via Arbox as a sanctioned middleman — see Addendum. Purpose (b) has its own go/no-go gate (FIT-269) that must pass before M2 foundation work starts.

Context

We already ship a one-time Arbox import (FIT-34: plans, members, membership enrichment; apps/api/src/import/). The gap is what happens after import: the client runs two systems during the transition, and everything done in Taikan is invisible in Arbox. That kills confidence — the owner cannot trust Taikan until the day they fully cut over, so they never start.

Parity mode inverts the risk: the client starts working in Taikan while Arbox stays correct. New members, membership assignments and class bookings made in Taikan appear in their Arbox minutes later; front-desk staff who still live in Arbox see a coherent picture; if the client aborts the migration, their Arbox is intact. It is a bridge, not an integration tier — success is the client turning it off.

What the Arbox public v3 API gives us (verified against the official OpenAPI spec, 46 paths):

  • Writes: POST/PATCH /v3/users; POST /v3/users/memberships (+ update, cancel, hold); POST /v3/schedule/bookSession (requires membership_user_id), POST /v3/schedule/cancelUserBooking, trial-booking create/delete; PATCH /v3/schedule (coach reassignment only); POST /v3/leads; POST /v3/users/recurringPayments (CHARGE/CANCELLED); push messages.
  • Reads: paged users / memberships / membershipTypes / locations / schedule (from_date+to_date, registration_details=1), searchUser by email/phone/id, 44 report endpoints with date windows.
  • Hard absences: no webhooks, no rate-limit contract, no idempotency keys, no class/session creation, no delta (“updated since”) queries, membership list returns type names not ids, all values typed string|null.

Codebase facts that shape the design (survey 2026-07-25): only external-id column today is member_profiles.arbox_user_id (non-unique, members only); no outbox and no domain events for most mutations (only membership.activated, booking.attended/no_show, subscription.renewed, lead events); integration_connections already models per-org external connections with status lifecycle + encrypted credentials; automations engine (ADR-0011) provides the listener/circuit-breaker/idempotency patterns to copy; importer persists plaintext credentials into import_jobs.config (pre-existing leak this work must fix).

Decision

D1. Sync topology: single-master per domain

DomainMaster during parityFlowNotes
Members (create/update)Taikanmirror → Arbox (POST/PATCH /v3/users, send_welcome_email:false)Arbox-side new members ingested by poller as Taikan members (flagged origin)
Memberships assign/cancel/holdTaikanmirror → Arbox (₪0 bridge type)never touch Arbox-priced rows except owner-initiated cancel/hold
Class bookings & cancellationsTaikanmirror → Arbox (bookSession / cancelUserBooking)inbound Arbox bookings ingested by poller
Schedule (classes)Arboxpoll → Taikan (windowed GET /v3/schedule)API cannot create Arbox classes; Taikan-created sessions stay local, badged
LeadsTaikan (optional, default OFF)mirror → Arbox POST /v3/leadsmost clients run leads only in Taikan
Paymentsn/anever syncedreconciliation report only
Attendance/check-inTaikan (best effort)none in v1no v3 check-in endpoint; revisit after spike

“Parity” therefore means: work in Taikan, Arbox follows; classes still authored in Arbox until cutover. This is honest about the API’s shape instead of promising bidirectional magic we cannot deliver.

D2. Connection & credentials

  • Add 'arbox' to integration_provider enum; one integration_connections row per org (status: pending|active|revoked|error, encrypted_credentials = AES-256-GCM via existing CredentialEncryptionService, external_account_id = Arbox box/location id from GET /v3/locations).
  • The import module keeps import_provider_configs; connect-flow can seed one from the other so the client pastes the key once. (The v3 api-key may or may not equal the v2 apikey — spike question; UI treats them as potentially distinct.)
  • Fix the pre-existing leak: ImportService must stop persisting resolved plaintext credentials into import_jobs.config (redact at write, not only at read).

D3. Identity map — arbox_entity_map

{ id, organization_id, entity_type ('member'|'membership_type'|'membership'|'schedule'|'booking'|'lead'|'location'|'staff'), taikan_id text, arbox_id text, origin ('import'|'mirror'|'poll'|'manual'), metadata jsonb, created_at, updated_at } with UNIQUE(organization_id, entity_type, arbox_id) and UNIQUE(organization_id, entity_type, taikan_id), indexed both ways.

  • Backfilled from member_profiles.arbox_user_id + import lineage on enable.
  • This table is the echo-loop breaker: the inbound poller skips any Arbox entity whose id is already mapped; the outbound mirror skips any Taikan entity whose origin is poll for the same change generation. Identity is never inferred from email/name once mapped.
  • Ambiguous matches (searchUser returns >1) are never auto-linked — they park into the issues inbox for manual resolution.

D4. Outbox — arbox_sync_ops + arbox-mirror queue

{ id, organization_id, op_type ('user.create'|'user.update'|'membership.assign'|'membership.update'|'membership.cancel'|'membership.hold'|'booking.create'|'booking.cancel'|'lead.create'), aggregate_key (e.g. member uuid — per-aggregate FIFO), dedupe_key unique-nullable, payload jsonb, status ('pending'|'in_flight'|'succeeded'|'failed'|'parked'|'superseded'), attempts, last_error, run_after, created_at, completed_at }.

  • Producers: ArboxMirrorListener (@OnEvent on domain events, automations-listener pattern — every handler try/caught so a mirror bug can never break the member-facing request).
  • Consumer: BullMQ arbox-mirror processor (ADR-0009 conventions, ObservableWorkerHost). Ordering: an op is only eligible when no earlier op with the same aggregate_key is unfinished — enforced by the dispatcher query, not queue order. A parked op blocks its aggregate’s later ops (create → assign → book is a hard chain) but never other aggregates.
  • Idempotency without server support: verify-then-apply. Before user.create, searchUser by normalized email then phone; after an ambiguous failure (timeout), re-verify before retry. Write returned Arbox ids into arbox_entity_map in the same DB transaction as marking the op succeeded.
  • Retry: exponential backoff (1m/5m/30m/2h/6h), then parked + issues-inbox entry. Org-level circuit breaker (copy automation-circuit-breaker.service.ts): sustained failure ratio pauses the org’s mirror, banner in UI, auto-probe half-open.
  • Rate limiting: per-org token bucket in the client (default ≤2 req/s, configurable via env) since Arbox documents no limits.
  • Delivery guarantee: at-least-once from outbox onward; the commit→listener gap is closed by reconciliation (D6). We explicitly did not choose a transactional outbox written inside every service transaction — see Alternatives.

D5. New domain events (needed regardless of Arbox)

member.created, member.updated (users/memberships services + lead-convert), booking.created, booking.cancelled (incl. admin cancel + waitlist promotion), subscription.created, subscription.cancelled, subscription.frozen/resumed — emitted post-commit at the attachment points already identified in the survey (bookings.service.ts book/cancel/adminCancel/promoteFromWaitlist; memberships.service.ts accept/join/convert paths; subscriptions.service.ts lifecycle methods). Automations and analytics want these anyway; the mirror is just another subscriber.

Superseded detail. promoteFromWaitlist was replaced by fillFreeSeats; promotions are announced post-commit as the booking.promoted domain event (apps/api/src/bookings/booking-events.ts). See scheduling-bookings › Waitlist.

D6. Inbound poller + reconciliation

  • Fast loop (5–10 min, flag-tunable): GET /v3/schedule?from_date=today&to_date=+14d&registration_details=1 → upsert Arbox-originated classes/bookings into Taikan (origin poll in the map). This is what makes Arbox-side front-desk activity visible in Taikan.
  • Slow loop (nightly): paged GET /v3/users + GET /v3/users/memberships full scan → (a) ingest new Arbox-side members, (b) drift report: mapped entities whose state diverges (member active/inactive mismatch, membership dates, double-active-paid-membership watchlist), (c) re-drive any Taikan mutation that missed its mirror (compares Taikan state vs map vs Arbox state).
  • No updated_since API exists, so full scans are the only option; the token bucket and nightly cadence keep load polite. Reports (bookingsReport etc.) may offer cheaper deltas — spike question.

D7. Billing ownership (the one rule that must never break)

  • Each member has exactly one billing owner: arbox (legacy recurring stays in Arbox) or taikan (Morning). Stored per member (map metadata), surfaced in UI.
  • Mirrored memberships always use the client-created ₪0 non-recurring “Taikan” membership type in Arbox (preflight enforces it exists before enabling membership/booking mirror). Arbox therefore never charges for anything Taikan sold.
  • Taikan never calls POST /v3/users/recurringPayments automatically. A guided, explicit, per-member owner action (“move billing to Taikan”) may cancel the Arbox recurring payment — logged, confirmed, reversible-instructions shown.
  • The nightly drift report flags any member with active paid memberships in both systems.

D8. Failure semantics & product guardrails

  • Taikan request paths never await Arbox. Mirror is eventually consistent; UI states the expected lag (“appears in Arbox within minutes”, “Arbox changes appear within ~10 minutes”).
  • Capacity race (class fills in Arbox after Taikan showed space): Taikan booking stands; failed mirror parks with a staff-facing resolution card. We do not auto-cancel member bookings.
  • Kill switches: disconnect stops writes and purges queued payloads (PII hygiene) while keeping the map for audit.
  • Non-prod safety: the Arbox client refuses to run unless NODE_ENV=production or an explicit ARBOX_SANDBOX_ALLOW env allowlist matches — e2e/dev must never write to a real client box (same incident class as the e2e DB wipe).

D9. Gating & rollout

  • PostHog flag arbox-parity-mode, organization group, default OFF, fail-closed (unevaluable ⇒ mirror paused, Taikan unaffected) — per the feature-flag policy; reference implementation WebhookEnforcementService. (Retired: the flag reached every organization and was removed; the bridge is owner-only, and an org’s own connection and its dry-run mode are its switch.)
  • Platform tier: enforce the already-defined-but-unenforced arbox_migration feature for import, add arbox_parity for Bridge (ADR-0008).
  • First org: Erez’s box only after the sandbox spike passes and dry-run mode (ops generated + logged, not sent) has run clean for several days.

Consequences

Positive

  • Removes the single biggest onboarding objection (“I can’t risk my Arbox data”); the client can adopt Taikan incrementally with a visible safety net.
  • The domain events, entity map and outbox are durable platform assets (MOVE/FreeFit integration FIT-65, future webhooks, analytics) — not Arbox-specific throwaways.
  • Single-master-per-domain + ₪0 bridge memberships keep the failure modes boring: worst realistic case is stale, not corrupted or double-charged.

Negative / costs

  • We build on a competitor’s unversioned (0.0.1), webhook-less API that they can throttle, gate or revoke at any moment; parity must always degrade gracefully to import-only, and we must never market it as guaranteed. Legal posture: the box owner supplies their own key and consents to Taikan acting as their agent on their own data — review Arbox ToS before first paying client.
  • Polling full member lists nightly is inelegant and caps practical org size (fine ≤ a few thousand members; revisit if a big box signs).
  • Two new tables, a queue, a poller and ~8 new domain events is real surface area (~estimated 4–6 weeks across phases). (Amended: originally justified as a feature whose success ends in its own disablement; the Move reframe makes it standing infrastructure for Move-dependent gyms — see Addendum.)
  • The commit→listener gap means a crashed process can delay (not lose — reconciliation re-drives) a mirror write; we accept up to 24h worst-case for the rare crash window instead of invasive per-transaction outbox writes.
  • Attendance/check-in parity is out of scope v1 (no endpoint); Arbox entrance reports remain the client’s check-in record until cutover.

Alternatives considered

  1. Periodic re-import only (one-way refresh). Rejected: does not satisfy the actual need — users acting in Taikan must appear in Arbox, or staff cannot trust either system.
  2. True bidirectional field-level sync with conflict resolution. Rejected: no webhooks, no deltas, no idempotency on the Arbox side makes correct bidirectional merge undeliverable; single-master per domain is what the API can honestly support.
  3. v2 User-API write path (POST /scheduleUser/insert hack from discovery). Rejected as primary: undocumented, requires member-level or staff email+password credentials; the public v3 API is documented, key-authed and covers users/memberships/bookings. v2 stays a read-only fallback for gaps (e.g. reports).
  4. Transactional outbox written inside every service transaction. Rejected for v1: touches every mutation path invasively; listener-produced outbox + nightly reconciliation gives at-least-once with far less blast radius. Revisit if reconciliation-caught gaps show up in practice.
  5. Building mirror logic into the automations engine. Rejected: automations are user-configurable org logic; parity is system infrastructure with different failure semantics (FIFO per aggregate, circuit breaker to a partner API). We copy its patterns, not its runtime.
  6. Mirroring payments/charges. Rejected outright: double-billing risk dwarfs the reporting benefit; reconciliation report instead.

Addendum (2026-07-26): Move/FreeFit middleman reframe

During the Move Integration consolidation the bridge picked up a second, strategically distinct purpose.

Context. Move/FreeFit (Movement Group) gates its booking integration to approved gym-management systems and will not onboard Taikan directly (FIT-65). For gyms already on Arbox, the bridge lets Arbox act as a sanctioned middleman: the gym runs Taikan day-to-day, Arbox keeps the class grid and the Move connection, and the bridge syncs between them. Move members keep booking through Arbox’s Move link; Taikan-side bookings flow into Arbox and (assumption) decrement Move-visible capacity.

Load-bearing assumption — validated before M2, not assumed (FIT-269, Urgent, milestone M1):

  1. Bookings written into Arbox via POST /v3/schedule/bookSession propagate through Arbox’s own Move link (capacity decrements on the Move surface).
  2. Move-originated bookings are visible to us in GET /v3/schedule?registration_details=1 — and with what identity (real member record vs anonymous placeholder). If either fails, the Arbox-as-middleman thesis is NO-GO and M2–M5 are re-scoped before any schema lands. Evidence must be observed (Move-surface screenshots + raw payloads), not vendor assurances.

What the reframe changes in this ADR:

  • Graduation is now conditional. For migration-only gyms, the original framing holds: success = the client turns the bridge off. For Move-dependent gyms, the bridge is standing infrastructure until Taikan obtains a direct Move slot (timeline/cost tracked in FIT-269 Q6) — classes stay authored in Arbox indefinitely for those orgs, and the “Finish your move” UX must distinguish the two modes (a Move-dependent org “graduates” from migration while keeping the schedule/booking sync alive).
  • The spike grew (FIT-259 Q13/Q14 + fixtures exit bar): Q13 — are Move/FreeFit-originated registrations present in registration_details and counted in registration_count; Q14 — pagination semantics of GET /v3/schedule (the poller needs resumable cursors). Recorded request/response payloads are committed to the repo as test fixtures so the client (FIT-260) and poller (FIT-264) tests run against real shapes.
  • Design fork, resolved by Q11+Q13: mirror who is booked (full roster incl. Move members ingested as Taikan records) vs only a number (count-only display with a “managed in Arbox” badge). M4 (FIT-263/FIT-264) branches on the answer; do not pre-build either branch before the spike lands.
  • Milestones: the FIT-258 tree is organized as M1 Contract & Spike (FIT-259, FIT-269) → M2 Foundation (FIT-260/261/262/265) → M3 Read Sync (FIT-264) → M4 Bookings (FIT-263) → M5 Hardening & Rollout (FIT-266/267/270, incl. the Move end-to-end pilot FIT-270).
  • Additional legal surface: beyond Arbox ToS, whatever agreement exists between Arbox and Movement Group may constrain a third party driving the box’s Arbox account via API — reviewed as part of FIT-269 Q5 before the first paying pilot.

Endgame remains a direct Move slot. The bridge buys time and proves demand; FIT-269 Q6 re-opens the direct-integration conversation with Movement Group so we know how long the middleman has to live.

Addendum (2026-07-29): M1 results — thesis validated, three constraints corrected

M1 ran against the live Move-connected pilot box (read-only except one owner-approved book/cancel pair). Full evidence: docs/_archive/arbox-v3-validation.md; go/no-go recorded on FIT-269.

The load-bearing assumption holds — GO, both directions.

  1. An API-written bookSession decremented the Move app’s visible remaining capacity (badge went 4 → 3 spots, restored on cancel). Observed on the Move surface, not asserted by a vendor.
  2. Aggregator-originated bookings are visible to us with real identity: registration_Details (note the capital D — the spec’s prose says otherwise) carries user_id, name and phone per seat, discriminated by user_role: 'aggregatorMember', and is counted in registration_count (verified across 200/200 sessions). A 19-month sweep of attendanceExternalMembersReport (6,364 rows) found every aggregator visit funnelled through a single FREEFIT membership type; two known aggregator-app users were located by phone. There is no separate Move pipe and no anonymised placeholder.

⇒ The who-vs-count fork resolves to WHO. FIT-263/FIT-264 build the per-member roster branch. The count-only fallback stays documented but unbuilt.

Three things this ADR asserted that turned out to be wrong or narrower than stated:

  1. “The API cannot create classes” is true of the public v3 API only. The Arbox staff dashboard drives arboxserver.arboxapp.com/api/manage/v2/* with a login session and exposes schedule/createEvent, updateEvent, updateSchedulesBetweenDates, createOrUpdateEventType, duplicateEventType — plus scheduleUser/insert for bookings. So “Arbox stays schedule master” (D1, D8, the Taikan-only-class badge) is a consequence of the transport we chose, not of Arbox. See the transport note below.
  2. Attendance is readable inbound. D1 scoped attendance out for lack of an endpoint, but registration_Details.checked_in and bookingsReport.check_in both expose it. Arbox → Taikan attendance ingest is feasible in v1 and should be considered for FIT-264 scope; only the write direction is genuinely absent.
  3. Identity is messier than D3 assumed. Arbox does not dedup aggregator records against native members — one human legitimately holds two user_ids with the same phone (native, with email; aggregator, email null). searchUser by phone returning >1 is therefore normal, not an anomaly, and the poller must expect aggregator shadows of existing members. The never-auto-link rule stands and now has a concrete failure mode behind it.

Also corrected/confirmed at the API level: v2 and v3 share one api-key (the connect flow can reuse the import key, D2); a bad key returns a real HTTP 401, so the v2 “200-with-error-body” quirk does not reproduce on v3 (but /v3/customFields answers without the envelope, so the client must tolerate both); membershipTypes returns real ids, so plan mapping is id-based; GET /v3/users/memberships?user_id= reliably returns membership_user_id, which is the D4 fallback for Q10; bookSession’s success body is mostly nulls and its booking_id echoes the schedule_id, so bookings must be keyed on (schedule_id, user_id) and confirmed by the next poll rather than trusted from the write response; aggregator records are not writable via the api-key (400 user_id is not valid), which makes the D3 echo-guard structural for Move seats rather than something we enforce.

New constraint the ADR did not anticipate: tier economics. Arbox gates by plan — the api-key is Standard-only (₪406/mo) while the Move/FreeFit link needs only Basic (₪157/mo), and Basic caps the box at 20 classes/week. A gym that wants its whole grid on Move needs Standard regardless of us; a gym exposing only a Move-facing subset (the pilot box’s aggregator traffic touched ~14 sessions/week) could live on Basic, but Basic has no api-key. That makes the login-session path an economic question rather than the merely-undocumented alternative rejected in Alternatives §3.

Consequent design amendment — the transport seam. Everything in D3–D6 (entity map, outbox, per-aggregate FIFO, circuit breaker, poller) is transport-agnostic and unchanged. Introduce one interface with two implementations: ArboxApiKeyTransport (public v3, Standard boxes — build first, it is documented and validated) and ArboxSessionTransport (manage/v2 via the box’s login, Basic boxes). Per-org choice, config-driven. The session transport additionally exposes booking-notification control (sendInvitation, alongside send_push/send_sms/send_email in the adjacent messaging payload), which may resolve spike Q5 — the notification side-effect launch blocker the public API gives us no lever over.

Unresolved, and deliberately so: whether the session transport works on a non-Standard box (needs one hour on a paid playground box); the ToS posture of reproducing the staff client specifically to avoid a paid gate — heavier than using an issued key, and a business/legal call, not an engineering one (FIT-269 Q5); and the remaining write-semantics questions (Q3 dedup, Q4 delete, Q5 notifications, Q7 late-cancellation), all of which need a writable box. Status stays Proposed until Saar signs off per FIT-267.

  • ADR-0004 (org isolation), ADR-0008 (tier gating), ADR-0009 (BullMQ jobs), ADR-0011 (automations engine — listener/circuit-breaker/idempotency patterns), FIT-34 (import), FIT-157 (automations import), FIT-65 (MOVE integration — future consumer of the same event/outbox infra).
  • Feature docs to be created at docs/features/arbox-bridge/ during implementation; runbook docs/runbooks/arbox-bridge.md (key rotation, circuit-breaker ops, disconnect).
  • Arbox v3 OpenAPI: https://arboxserver.arboxapp.com/docs/api.json; discovery notes docs/_archive/arbox-discovery-findings.md.

2026-09-21 implementation amendment: official API only

TKN-332 supersedes the earlier staff-session exploration: the Bridge must use only documented public v3 routes. No session transport, Management API, browser automation or fallback is permitted. The initial implementation focuses on schedule/roster import and bookings against explicitly linked, already-existing free non-recurring Arbox memberships. User creation and membership lifecycle mirroring remain outside this increment.

Booking intents are reconciled from committed state every minute using the existing durable scheduled-job infrastructure and per-org PostgreSQL advisory locks. This replaces the planned event-listener/worker pair for the booking-only increment and covers bulk cancellation and promotion without an event-delivery gap. Schedules are read every five minutes over today through +14 days. Dry run still imports schedules/rosters but sends no outbound bookings. An approved public-API write test reference is required to enable live writes; vendor notifications, penalties and credit behavior remain a pilot acceptance gate.

See the implementation and rollout guide for current behavior, intentional limits, code map and verification. Earlier sections describe the broader epic and are not a claim that all domains have shipped.

2026-09-26 amendment: local-first booking, member mirror, guided class cutover

Owner decisions that supersede the 2026-09-23 “confirm in Arbox first / bookings only” notes on TKN-258, TKN-263 and TKN-266.

Booking stays local-first (D8 reaffirmed). A Taikan booking, cancellation, credit and notification commit exactly as without the bridge; no request path calls Arbox. Confirming in Arbox first would add at least one bookSession call and a roster read to every booking and couple booking availability to Arbox uptime. The residual risk is the last seat: Arbox, front desk or Move can take it between the last poll and the mirror. To shrink that window, every booking mutation nudges a per-org sync job post-commit, so the mirror runs within seconds instead of on the next minute tick. When Arbox rejects a mirrored booking (full class, vendor rejection, later drift), the Taikan booking stands, the operation parks with a stable code, and the org owners receive a one-time alert. Members are never cancelled or told about the bridge.

Members and the free bridge membership are mirrored. TKN-332 forbids undocumented access, not documented public v3 writes. The bridge therefore creates and updates Arbox users (POST/PATCH /v3/users, never a welcome email) and assigns the owner-selected ₪0 non-recurring bridge membership type (POST /v3/users/memberships, cancelled via /cancel when the Taikan membership ends). Identity is verify-then-apply: search by email, then phone; exactly one native hit links, several hits park as identity_conflict, zero hits create and then re-search to obtain the id. Arbox-priced memberships, recurring payments, trials, leads, messages and deletes are never written. Every Taikan booking on an imported class can then reach Arbox, so Arbox and Move keep an accurate seat count during a gradual migration.

Classes: guided mirror with per-series cutover. Public v3 cannot create a class and can change only its coach (PATCH /v3/schedule). Arbox stays the author of Arbox-owned series: imported title, time, capacity and cancellation are locked in Taikan, and a coach change in Taikan is pushed when the coach is mapped to an Arbox staff member. A class created in Taikan becomes an owner task to create it in Arbox; the next poll holds an exactly matching Arbox class (same start, end and normalized title) as a candidate that the owner confirms or rejects, so no duplicate is imported. Per series (title, weekday, local start time) the owner can switch authoring to Taikan: the poll then links exact-start matches, never overwrites or cancels the Taikan class, and lists Arbox-only occurrences for the owner to remove in Arbox. Bookings keep mirroring on linked classes in both modes. A third setting marks a series Taikan-only.

Operations. Sync work runs on a dedicated arbox-sync queue, not the shared scheduled-jobs worker. Operations carry a correlation id and emit arbox_sync_op/arbox_sync_run events without personal data. Owner retry and dismiss are audited. Stored Arbox rosters keep no phone numbers and are purged on disconnect.