Skip to content

Guides & reference

One platform, not nine apps

The shared document layer: one Library, the Vault, cross-studio handoff, live sync across devices, and the team channel.
Sculptural study of connected forms and structured ideas

flowss grew as a constellation of workstations: Studio, Study (NEXUS), Atlas, Lab, LucidFlow, Science, BPMN, Sketch. Each is strong alone, and the platform already shares a spine — the workstation registry (lib/studio-handoff.ts), the ⌘K palette (lib/command-registry.ts + GlobalCommandPalette), and the dashboard's work index (lib/dashboard/work-index.ts).

Continuum closes the remaining gaps that make the studios feel like separate apps rather than rooms of one building:

GapBeforeWith Continuum
MemoryStudio has a real figure library; every canvas studio (LucidFlow, Science, BPMN, Lab, Sketch) remembers exactly ONE documentThe Continuum library (lib/continuum/store.ts) — every studio's documents live in one canonical, multi-document library with rename/duplicate/soft-delete, surfaced at /library and on the dashboard. The Vault layers explicit checkpoints on top: a frozen copy of a studio's live keys you can restore on demand (think local history), which also powers cross-device sync pings
ContinuityLeaving a studio means losing the thread; the only cross-studio view is /dashboardPulse — a cross-studio recents feed, surfaced by the Flow Dock (⌘J) that floats in every workstation, and a "Continue" group at the top of ⌘K
LineageHandoffs (Send to studio…) deliver the work but forget the journeyLineage — every handoff is recorded; the dashboard shows how work flowed Sketch → Studio → Atlas → Study
Search⌘K knows nav/engines/templates/Studio figures — not the rest of your workWorkspace — one enumerator over the Continuum library (every studio's documents) + vault checkpoints + NEXUS courses, searchable from ⌘K everywhere
StudyDiagrams and NEXUS courses never meetStudy this diagram — any diagram can be handed to NEXUS as course notes; the course creator picks it up and builds a diagram-first course from it

Architecture

All of it lives in lib/platform/ (pure, unit-tested, node-safe) plus one shell component:

  • lib/platform/types.ts — shared types.
  • lib/platform/vault.ts — snapshot/restore named CHECKPOINTS. A snapshot is a copy of the studio's live localStorage keys (the exact same channel discipline studio-handoff.ts established); restore writes them back and navigates. glyph:vault:v1. The living document list is the Continuum library (lib/continuum/store.ts) — the vault never replaces it.
  • lib/platform/pulse.ts — bounded cross-studio recents ledger, written by the shell on studio entry and by handoffs/vault restores. glyph:pulse:v1.
  • lib/platform/lineage.ts — bounded handoff ledger. glyph:lineage:v1.
  • lib/platform/workspace.ts — merges the dashboard work index, the vault and the pulse into one searchable WorkspaceItem[] for ⌘K and the dock.
  • components/platform/FlowDock.tsx — the continuity surface. Mounted from components/platform/PlatformBridges.tsx (the root layout's one client host) on workstation routes **and on /library, /flow and /dashboard** — the two cross-studio surfaces are about continuity, so a dock that was absent from exactly them made ⌘J read as unreliable. Without a studio it drops the studio-specific bands (checkpoint save, "Take it further") and keeps Continue, the Vault list, Journey and the phase reading.
  • components/platform/ContinuumBoard.tsx — the same three bands laid out horizontally above the Library board. It shares the dock's vault-delete path (lib/platform/vault-delete.ts) and the platform's one "how long ago" (lib/platform/time-ago.ts); it still has its own markup, which is the remaining duplication in this layer.

Every module uses an injectable KV seam (defaults to window.localStorage) so the logic is fully testable under the node vitest environment, and every write is guarded — storage failures degrade to no-ops, never to crashes.

Why localStorage-key snapshots (and not a rewrite)

Each canvas studio already treats its localStorage key as its document of record (verified in lib/dashboard/work-index.ts). Copying those keys is therefore a complete snapshot by construction, and restoring them is exactly what the studio does on mount. The vault gets multi-document behaviour platform-wide without touching any studio's persistence code — the same "deliver through the channel the studio already understands" principle the handoff module proved out.

Live across devices

components/platform/ContinuumSync.tsx joins the per-user Realtime channel continuum:<userId> (same infra as CollabBridge): Pulse and Lineage ledgers broadcast whole (tiny, bounded) and union-merge idempotently on arrival; Vault changes send a metadata ping and receivers pull /api/cloud-sync after the sender's fast-debounce push lands. Merges write + notify only on real change, so echo loops converge by construction. Outbound broadcasts are throttled per event with BOTH edges — leading-edge-only dropped the tail of a burst, and a send that writes lineage and then pulse a millisecond later is a burst by construction, so the last state never reached the other device.

The library across devices

The cloud payload (components/CloudSyncBridge.tsx → /api/cloud-sync) carries four things: Studio's figure workspace, every document-keeping studio's live keys, the Vault, and — since the gap below was found — the Continuum library itself.

That last one was missing entirely. The blob had no glyph:continuum:v1 in it, so a second device signed into the same account got one canvas per studio, re-adopted by the legacy sweep under brand-new ids, and a Library that had never heard of the user's titles, tags, stars, provenance or trash — under a product that advertises whole-platform sync.

It cannot ride as another opaque canvas key, because those merge as one value each (a three-way merge per key against the copy both sides last agreed on, with a conflicting second copy set aside in the Vault), and a library is a set: a whole-value rule leaves one device's documents out. So it merges, and the rule is pure and lives in lib/sync/reconcile.ts (mergeLibraryIndexes):

  • union by id — a document either device knows about survives;
  • last-write-wins per document on updatedAt, ties to local (which keeps changed: false, so nothing is re-persisted or re-broadcast — that answer is what stops two idle laptops becoming a broadcast loop);
  • soft deletes need no special case: deletedAt travels on the document and trashing bumps updatedAt. The one addition is an exact-tie rule, shared with the cross-tab merge: on equal stamps the trashing wins, but never over an edit made after it;
  • hard deletes DO need one, because the document is gone and only the purge ledger remembers. A tombstone suppresses the document on both sides — unless it has been edited since the purge, in which case it was re-created and the tombstone goes instead (the store's own invariant: a document and its tombstone never coexist).

lib/continuum/store.ts owns the two ends: librarySnapshotForSync() never speaks for a library it has not loaded (an unhydrated route returns null, because a POST replaces the blob and an empty push would erase the account's library), and mergeRemoteLibrary() merges, commits and flushes.

It rides on a budget, and the budget is not optional. The library carries every document's source and data, and it goes up alongside the canvas bag (1.2 MB of the route's 2 MB cap) and the figure workspace. Sent whole, a real library takes the payload over the cap — and decidePush then blocks the entire push, so a user who previously had their figures and Vault backed up would end with nothing backed up at all. planLibraryDocs (lib/sync/reconcile.ts) therefore keeps the newest documents that fit LIBRARY_DOCS_MAX_BYTES and reports the rest through the same skipped-document channel a canvas doc uses. Whole documents only: a hollowed-out copy would land under a live id on the other device and win the merge, so a document either crosses intact or does not cross — and because the merge is a union, not crossing loses nothing on either side.

The realtime channel map

Every Supabase Realtime channel the platform opens, in one place. Each namespace prefix is distinct, so no two features can ever land on the same channel — new features must pick a fresh prefix and add a row here.

ChannelOpened byCarries
sketch-live:<sessionId>Sketch "Share live" (lib/live/useLiveSession)Element sync (LWW items) · cursors · comments · voice
lucidflow-live:<sessionId>LucidFlow "Share live"Node/edge sync · cursors · comments · voice
science-live:<sessionId>Science "Share live"Node/edge sync · cursors · comments · voice
bpmn-live:<sessionId>BPMN "Share live" (lib/live/useLiveDoc)Whole-doc sync (pen-coordinated) · pen · comments · voice
lab-live:<sessionId>Lab "Share live" (lib/live/useLiveDoc)Whole-doc sync (pen-coordinated) · pen · comments · voice
studio-live:<sessionId>Studio ad-hoc "Share live" (lib/live/useLiveDoc){engine, code} sync (pen-coordinated) · pen · preview cursors · voice
project-voice-live:<projectId>RetiredPrivate presence/voice temporarily unavailable
atlas-live:<projectId>RetiredPrivate presence/voice temporarily unavailable
project-sync:<projectId>CollabBridge (cloud projects)Empty change hints; documents use authorized HTTP
room:proj:<projectId>RetiredPrivate project cursors temporarily unavailable
figure-presence:<projectId>RetiredPrivate figure presence temporarily unavailable
project-comments:<projectId>lib/hooks/useProjectCommentsEmpty hints; comments use authorized HTTP
continuum:<userId>ContinuumSyncPulse/Lineage merge + Vault change pings
org-continuum:<orgId>RetiredTeam presence/source handoffs temporarily unavailable

The <studio>-live:* rooms are the shareable multiplayer sessions — lib/live/session.ts derives the name from the studio slug, and lib/platform/live-now.ts announces whichever one this page is running so the Flow Dock shows a "Live" row (peer count · copy link · leave). Private project rooms are no longer auto-joined.

The team layer

Team presence and person-to-person source handoffs are temporarily unavailable. TeamPulse clears the roster and refuses handoffs without opening a channel. Saved team projects, permissions and component libraries remain available. Cross-studio handoffs within the user's own workspace are unchanged. See private collaboration cutover (launch/PRIVATE_REALTIME_CONTAINMENT.md).

Context verbs

The Flow Dock's "Take it further" chips read the current studio's live document and hand it onward through performSend — Research it (Atlas), Study it (NEXUS), or a canvas/code hop — the exact same pipeline as the Send-to-studio menus, so conversion and lineage recording ride along.

Event flow

studio entry ──────────────► pulse.touch(studio, title?, engine?)   (the dock's route effect)
Send to studio… ───────────► lineage.recordHandoff(from → to)       (only when `from` is a studio)
                              …then the ARRIVAL is what touches the Pulse,
                              via the dock's studio-entry effect above
Vault save ────────────────► vault.checkpointStudio(studio, title)
Vault restore ─────────────► write keys + detachActive + pulse.touch(restored studio) + navigate
Vault delete ──────────────► vault-delete.deleteCheckpoint(id) — one click, undoable for 6s
library write lost ────────► store PERSIST_FAILED_EVENT → sticky `local` sync status + one toast
library write recovers ────► store PERSIST_RECOVERED_EVENT → the banner comes down
⌘K search ─────────────────► workspace.listWorkspace()  (figures + docs + vault + courses)
⌘K on a document ──────────► deliver.openInHomeStudio(doc) — one hop, not via /library
⌘J ────────────────────────► Flow Dock (recents · vault · lineage)
⇧⌘F on /study ─────────────► Study's cross-course concept search (⌘K is the platform palette
                              everywhere now, Study included)
Study this diagram ────────► localStorage glyph:nexus:handoff:v1 → /study
                              CourseCreator prefills notes from the payload

Two honest notes about that first pair, because the previous version of this document claimed more than the code does:

  • recordHandoff needs a studio of origin. inferSourceStudio() resolves the current URL through lib/platform/routes.ts (so /scholar and /biology resolve, which a private copy of the table here used to get wrong for /scholar), and returns null off a workstation — so a send launched from /library, /flow or a dashboard card records no lineage row. Giving those an origin means widening LineageEvent.from beyond the workstation registry, which is a change to the ledger's shape rather than a fix, and is not made here.
  • Nothing calls pulse.touch(to) at send time. The touch happens on ARRIVAL, from the dock's studio-entry effect, which reads the target's live keys — so the row carries the delivered document's own title and engine instead of the sender's guess at them. The old wording described a call that does not exist.

Placement

Bottom-right is the Flow Dock's pill (draggable, position persisted per browser; the panel is capped at min(21rem, 100vw − 2rem) so it cannot hang off a narrow screen). Bottom-centre is the Toaster. Bottom-left is the sync-health banner (components/platform/SyncHealthBanner.tsx), which reports cloud failures AND the sticky local state for a browser that has stopped saving — it is mounted beside the storage alarm rather than inside CloudSyncBridge, because a signed-out visitor whose storage is full needs it just as much.

Something unclear or out of date on this page? Tell us from the Support link in any studio — the flowss team reads every report.

© 2026 Voranox Inc. flowss — Flow Systems Studio. All rights reserved.

This documentation, its text and its examples are protected by copyright. Engine and format names are trademarks of their respective owners — see the terms and copyright and licences.