V1 Web PWA · Journey

Journey: Multi-device workspace handoff — one-way packet to native Home

A source-reconciled journey record for V1 Web PWA — shipped behavior, state boundaries, failure modes, and the automation evidence available today.

automated · 5 specs verdict: partial· 2026-05-29
9sections10 minread

On this page
Journey at a glance
ActorsDesktop researcherNative mobile commuterSame-browser testerResponsive web reader
Automation5 E2E spec(s) · verdict: partial· 2026-05-29

Journey flow#

Generated from the authored steps below — click a node to jump to that section.

flowchart TD s1["1. Open the real workspace route"] s2["2. Prepare the desktop frame"] s3["3. Build the six-item packet"] s4["4. Capture locally, then launch delivery"] s5["5. Store one opaque packet per user"] s6["6. Fetch on native Home"] s7["7. Resume and consume best-effort"] s8["8. Keep the two deep links distinct"] s9["9. Treat Library and profile sync as separate…"] s10["10. Attempt the return lane"] s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> s8 --> s9 --> s10 click s1 href "#1-open-the-real-workspace-route" click s2 href "#2-prepare-the-desktop-frame" click s3 href "#3-build-the-six-item-packet" click s4 href "#4-capture-locally-then-launch-delivery" click s5 href "#5-store-one-opaque-packet-per-user" click s6 href "#6-fetch-on-native-home" click s7 href "#7-resume-and-consume-best-effort" click s8 href "#8-keep-the-two-deep-links-distinct" click s9 href "#9-treat-library-and-profile-sync-as-separate-evidence" click s10 href "#10-attempt-the-return-lane" s10 --> v(["verdict: partial"]) click v href "../results/multi-device-workspace-handoff.html" style v stroke:#d6a531,color:#d6a531

The shipped cross-device leg carries one latest desktop workspace packet through an authenticated per-user BFF mailbox to native Home. A separate browser-local path supports capture and test-injected web resume. This is not a round trip: desktop success copy is local-only, mobile consume is best-effort, and Send back to web opens an assistant prompt instead of persisting the derived mobile_to_web packet.

Personas#

  • Desktop researcher — packages the active workspace tab, reading, evidence, and panel routes for mobile.
  • Native mobile commuter — refreshes Home under the same BFF account and resumes the primary packet item.
  • Same-browser tester — captures or injects a packet through one localStorage key without proving another device received it.
  • Responsive web reader — follows a web state URL at 390 px; this is a mobile-width browser path, not the native Expo consumer.

Pre-conditions#

  • Web and native Home are authenticated as the same BFF user for a real cross-device receive.
  • /workspace/... can load its active tab. Nisaba notebook anchors may be live; the broad browser evidence still routes a Veritas saved-article fixture because authenticated setup has no public writer for that record.
  • PUT, GET, and DELETE /v1/iris/mobile-handoff are reachable. The production BFF requires the shared snapshot store and hydrates it before routes open.
  • The native Home refresh uses a BFF client with the account bearer. The mobile Library card is a separate fixture and must not be used as receipt proof.

Steps#

1. Open the real workspace route#

  • Use /workspace or /workspace/<path>. (workspace) is a Next route group and is not part of the public URL.
  • /app/<surface>/<path> is a canonical shell alias. Bare /app has no [surface] and is not a workspace handoff destination.
  • Authentication gating preserves the requested workspace path as the Welcome redirect target.

2. Prepare the desktop frame#

  • Select an active tab, panel, reading anchor, evidence source, side-panel kind, and side-panel width.
  • Tabs, selected reading/evidence ids, and notes persist under workspace browser storage. The separate web state URL carries path, tab, panel, side, and width.
  • At MAX_WORKSPACE_TABS, adding another tab evicts the oldest, updates the active state URL, and recomputes item ids and routes for the surviving active tab.

3. Build the six-item packet#

  • buildWorkspaceIrisMobileHandoffState creates reading, study, journal entry, voice session, assistant transcript, and notebook sync items.
  • Each item carries web/mobile route material, continuity markers, an update timestamp, and selected reading/evidence ids when those live values exist.
  • The shared contract derives readiness, a deterministic token, a nominal 48-hour expiresAt, an oshun://handoff/<token> URI, and memory-write-shaped projections.
  • The continuity block is static: oshun-v1-workspace-user, scopes, consent, active domain, and memory counts are not read from the authenticated member.
  • The displayed memory-write count is computed from the packet. No send handler commits those projections to Iris memory.

4. Capture locally, then launch delivery#

  • Click Send workspace to mobile.
  • Web first serializes the packet to oshun.iris.mobile-handoff.v1. Only local success renders Iris state captured at... and permits the network call.
  • Web then launches a fire-and-forget authenticated PUT to /v1/iris/mobile-handoff and does not inspect the HTTP response.
  • Local quota/private-mode failure prevents both badge and request. Network, auth, contract, size, or server failure after local capture leaves the badge visible and produces no delivery error.

5. Store one opaque packet per user#

  • The BFF derives the mailbox key from request.authContext.userId, not the packet's embedded static userId.
  • It rejects an absent/non-object/array state, non-canonical JSON hazards, and objects above 256 KiB measured as UTF-8, then serializes replacement through candidate/save/publish and returns storedAt only after the snapshot commits.
  • It does not fully validate direction, embedded identity, tenant, item ids, or routes. Client parsing supplies those semantics later. It does recognize the exact shared contract/version solely to enforce canonical expiresAt.
  • Production requires the durable sink. Failed persistence returns stable 503 without publishing the candidate; startup strictly restores schema v1 or a validated legacy document and fails loudly on unknown or malformed state.
  • Exact-subject continuity export, signed account erasure, 30-day last-access retention, exact-contract expiry, and a digest-only post-erasure write fence are part of the same durable partition.
  • The route has no device id. /v1/devices separately lists registered durable device tokens and is not consulted by this mailbox.

6. Fetch on native Home#

  • Home calls GET /v1/iris/mobile-handoff when its refresh timestamp changes.
  • The mobile client stringifies the returned object and parses it through the shared Iris contract. Missing, failed, or invalid data renders No cross-device handoff waiting.
  • The client parser requires expiresAt but does not compare it with current time. Before returning an exact shared-contract packet, the BFF compares the canonical timestamp, durably removes an expired record, and also applies the continuity-state 30-day access window through its retention worker.

7. Resume and consume best-effort#

  • Resume on mobile uses the primary non-blocked item, remembers recognized domain context, and routes with the handoff token and item path.
  • Home launches DELETE /v1/iris/mobile-handoff without awaiting it.
  • The shared mobile HTTP transport omits JSON content type for this bodyless DELETE, so Fastify does not reject it as an empty JSON request before routing.
  • Navigation succeeds even when delete fails. The card is not cleared in local state first, so the same packet can reappear on a later Home refresh.
  • Five route cases, eleven store cases, a real-PostgreSQL integration case, and a phased Chromium API proof cover adjacent-user isolation, failed-write nonpublication, process restart, durable consume, and consumed absence after a second restart. They do not make Native Home's unawaited request exactly once.
  • Packet deepLink is the custom oshun://handoff/... token URI. The token test checks its scheme, host, direction, source, and path.
  • The workspace also renders a relative /workspace/...?... state link. That is the link used by the 390 px browser-restoration test.
  • URL parameters restore tab, panel, side panel, and width. Selected reading and evidence remain primarily in oshun.workspace.tabs.v1.
  • The two-context browser test explicitly copies that desktop tab storage into the mobile-width context before asserting exact evidence equality. The web URL alone does not transfer selected content between device profiles.

9. Treat Library and profile sync as separate evidence#

  • Native Library always constructs an authored Bhagavad Gita packet with buildMobileHomeIrisHandoffState(). Its populated card is not fetched from the mailbox.
  • The authored Maestro flow first asserts the real Home empty branch, then moves to Library to inspect all six fixture channels and the send-back control.
  • HomeMultiDeviceContinuityPanel and the two-context profile-preference-sync case prove profile/preference refresh states. They never read the workspace mailbox and are not a packet receipt.

10. Attempt the return lane#

  • buildMobileReturnIrisHandoffState reverses direction to mobile_to_web and refreshes timestamps.
  • Native Home and Library calculate its projected write count, but the current handler does not PUT that packet. It opens the assistant with a prompt naming the token and count.
  • Web never fetches a return packet. It can render/apply mobile_to_web only when a component or Playwright setup writes serialized state into the same localStorage key.
  • No revision, conflict, replay, or merge contract carries mobile edits back to the desktop tab.

Post-conditions#

  • Local capture proves only that one browser stored a serialized packet.
  • A successful BFF PUT means one latest opaque packet committed for the same authenticated user and is recoverable after process restart.
  • A valid native Home GET can render and route the primary packet item.
  • Successful delete clears the mailbox; failed delete can replay it.
  • Mobile creates no server return record, and desktop receives no mobile edits.
  • /v1/devices, responsive web restoration, fixture Library, and profile sync remain separate authorities.

Failure modes#

  • Local write failure — no badge and no BFF PUT.
  • BFF failure after local success — badge remains; native Home receives nothing and web exposes no retry or error.
  • Blank/expired auth token — the request can fail while local capture still looks successful.
  • Malformed/oversized state — BFF returns 400/413; web ignores the response.
  • Backing snapshot failure — BFF returns 503 and publishes no candidate; the web currently discards that response and keeps its local capture badge.
  • Different account — per-user lookup returns no other subject's packet.
  • Static embedded user mismatch — BFF stores the opaque body under the auth subject but does not reconcile its embedded id.
  • Expired packet — an exact shared-contract packet is durably removed before BFF delivery; opaque or future contract versions receive only the generic 30-day continuity retention rule.
  • Delete failure — navigation proceeds and a later refresh can show the packet again.
  • State link without copied storage — responsive layout restores, but exact selected reading/evidence does not.
  • Library card mistaken for receipt — the visible populated card is an authored fixture.
  • Send back to web — opens an assistant prompt instead of delivering a return packet.
  • Concurrent edits — no merge or conflict model exists.
  • Deletion-fence capacity — account erasure writes a bounded digest fence; once its 100,000-fence capacity is reached the partition fails closed pending an explicit archive/migration policy.

E2E coverage#

  • workspace-multi-panel.spec.ts contains ten broad workspace cases, including local packet capture, injected mobile-to-web resume, local write failure, tab/note state, and responsive panels.
  • multi-device-handoff-deepening.spec.ts contains two cases for copied-storage 390 px web restoration and independent Home profile-sync states.
  • multi-device-deepening.spec.ts contains three cases for tab eviction, token/item recomputation, projected write count, and URL-plus-storage reload.
  • profile-preference-sync.spec.ts contains one genuine two-context account preference refresh case; it is not handoff transport.
  • iris-mobile-handoff-authority-bff.spec.ts contains three phase-selectable signed-auth API cases. Against the exact built BFF they prove durable seed, recovery and consume after one clean restart, and consumed absence plus adjacent preservation after a second restart.
  • iris-mobile-handoff.test.ts contains five same-process route cases for store/fetch/consume, isolation, auth, malformed input, UTF-8/prototype bounds, failed durability, and the deletion fence. The store adds eleven unit cases and one real-PostgreSQL restart/export/erase case.
  • Shared contract and mobile unit tests cover typed packet mechanics, all six route channels, return derivation, and card callbacks.
  • home-daypart-and-return-journeys.yaml asserts live Home empty state and then exercises the fixture Library card.
  • No automation drives the joined desktop PUT → native Home GET → consume → mobile edit → server return → desktop merge lifecycle.

Per-view files touched#

Cross-references#

Open questions#

  • Replace static continuity fields with live account-scoped Iris state.
  • Validate and subject-bind the shared packet contract at the BFF boundary.
  • Await the PUT and distinguish local capture, durable store, and device receipt in UI.
  • Join the real device inventory for target selection and per-device status.
  • Add device receipt/replay semantics above the durable serialized replacement and consume operations.
  • Surface durable acknowledgment and stable failure reasons in the web UI instead of discarding the BFF response.
  • Persist mobile_to_web, let web fetch/consume it, and define concurrent tab/note merge rules.
  • Define an archive or identity-reuse policy for the bounded digest fences.
  • Add native automation for the exact desktop → Home → desktop token and resulting state.