Skip to content

Latest commit

 

History

History
424 lines (363 loc) · 22.5 KB

ARCHITECTURE.md

File metadata and controls

424 lines (363 loc) · 22.5 KB

Underlay architecture

Last updated: 2026-10-27

Underlay is a Windows desktop-first, local-first infinite whiteboard. The product is delivered as a Tauri 2 application that hosts a React 18 + TypeScript interface around an embedded Excalidraw 0.18.1 canvas. No account, server, or network connection is required at runtime.

The repository is self-contained: source, tests, scripts, and documentation live at the root, the native shell is under src-tauri/, and isolated experiments under experiments/. Build output, caches, logs, test profiles, and development data stay in gitignored folders.

1. Goals and non-goals

In scope:

  • A real, mature drawing engine rather than a custom canvas reimplementation.
  • Infinite pan/zoom, direct text/sticky editing, freehand pen and highlighter, shapes, lines, arrows with bindings, selection, move, resize, rotate, group, duplicate, delete, undo/redo, clipboard text/images, import/export.
  • Automatic local persistence with recovery and edit-loss protection.
  • A Windows presentation layer: show/hide, desktop mode, click-through, tray, opt-in launch at login, and a normal-window fallback.
  • Local-only operation with no telemetry or cloud dependency.

Out of scope for the original build (the desktop canvas program now reopens the wallpaper-layer item):

  • Collaboration, accounts, sync, mobile, and macOS/Linux packaging.
  • Replacing Explorer. The desktop canvas program targets a child surface between the wallpaper and SHELLDLL_DefView; Explorer still owns icons, taskbar, Start, and shell menus.
  • Shell extensions, registry modifications outside an explicit launch-at-login opt-in, or permanent input capture.
  • Public binary or installer distribution (no signed release is published).

2. Stack and foundation decisions

Area Choice Why
Canvas engine Excalidraw 0.18.1 (MIT) Mature embeddable React component with the required whiteboard primitives: infinite canvas, hand-drawn shapes, bound arrows, text editing, selection/transform, undo/redo, clipboard, image export, and file import/export. Using the engine avoids a deep fork and a custom reimplementation.
UI layer React 18.3.1 + TypeScript 5.9 Excalidraw declares React 17/18/19 peers; React 18 is the pinned, tested combination. Strict TypeScript is enabled.
Build tool Vite 7.3.7 Fast dev server and a static production bundle embedded by Tauri.
Desktop shell Tauri 2.12.5 + Rust Small local application using the Windows WebView2 runtime instead of bundling a browser. Provides native window control, global shortcuts, tray, single-instance behavior, autostart, and file dialogs.
Storage JSON files on disk with atomic writes Transparent, versionable, easy to back up and inspect, and independent of any database service.

Alternatives considered

  • Electron. Mature and predictable because it bundles Chromium, but it adds a large runtime and installer, higher memory use, and a separate browser security surface. The official Excalidraw project deprecated its own Electron wrapper in favor of the web application, and the brief explicitly says not to deep-fork the engine. Tauri 2 was chosen instead; the trade-off is a dependency on the Evergreen WebView2 runtime and Windows-focused behavior.
  • Lively Wallpaper. A GPL-3.0 animated-wallpaper host. It was rejected: the license is incompatible with an MIT/Apache-oriented foundation for this project, and its implementation was not copied. Underlay built its own WorkerW host from documented/observable behavior in Phase 0 and Phase 1; the always-on-bottom window remains the fallback on unsupported builds.
  • "ExcaliSol". The brief named this as a candidate. A web search on 2026-10-26 found no credible public repository or documentation under that name; nothing could be verified, so it was not used.
  • tldraw. A capable React canvas, but its SDK license is not MIT/Apache and involves license keys/watermark terms depending on the license tier. Excalidraw was the cleaner fit for a permissively licensed local application.
  • A custom canvas. Rejected because it would duplicate years of engine work and fail the requirement to use a real, mature engine.

Sourcing and licensing evidence is recorded in docs/DEPENDENCY_AUDIT.md.

Excalidraw 0.18.1 exposes pen freehand but no separate highlighter tool (its public tool list contains freedraw, not highlighter). Underlay therefore adds a top-bar Highlighter action and a corresponding app-menu item that select the freedraw tool and set a wide (24 px), translucent (35% opacity), warm-yellow stroke before drawing. It is still ordinary Excalidraw freedraw data, so the marks persist, edit, export, and undo like any other element.

3. Layer map

+---------------------------------------------------------------+
| Windows desktop (Tauri 2 / Rust)                               |
|  - window modes, tray, hotkeys, autostart, single instance     |
|  - board/settings files, atomic saves, backups, trash, logs    |
|  - dialogs for import/export                                   |
+-------------------------------+-------------------------------+
                                | Tauri commands/events
+-------------------------------v-------------------------------+
| React application (TypeScript)                                 |
|  - App.tsx: boot, board state, save orchestration, dialogs     |
|  - TopBar, BoardLibrary, Settings, DesktopHint, Toast, etc.    |
|  - BoardStore abstraction: Tauri store | browser fallback      |
+-------------------------------+-------------------------------+
                                | onChange / initialData
+-------------------------------v-------------------------------+
| Excalidraw 0.18.1 (React component)                            |
|  - scene editing, drawing tools, selection, undo/redo, export  |
+---------------------------------------------------------------+

Frontend files

  • src/App.tsx — application orchestrator: boot, board lifecycle, save queue, desktop commands/events, shortcuts, dialogs, recovery messaging.
  • src/components/ — top bar, board library, settings, desktop hint, dialogs, toast, icons, brand.
  • src/store/ — BoardStore interface plus TauriBoardStore (IPC) and BrowserBoardStore (localStorage preview fallback).
  • src/lib/scene.ts — Excalidraw scene conversion, sticky-note creation, viewport/background sanitization, scene signatures.
  • src/lib/saveQueue.ts — debounced, serialized autosave queue.
  • src/lib/settings.ts — defaults and validation for settings, board presets, filename sanitization, time/size formatting.
  • src/lib/desktop.ts — typed wrappers over Tauri window, event, and autostart APIs.
  • src/styles.css — design system, layout, light/dark themes, Excalidraw bridge.

Rust files

  • src-tauri/src/lib.rs — Tauri builder, command handlers, tray menu, global shortcut registration, single-instance handling, data-root setup, app info.
  • src-tauri/src/store.rs — file format, atomic saves, backups, snapshots, trash, import/export, settings, logging, and unit tests.
  • src-tauri/src/desktop_surface.rs — desktop canvas host: WorkerW placement, scoped mouse hook, icon geometry and hit testing, WebView2 focus proxy, wheel bridge, attach/detach, startup wait, safe mode, and window-loss recovery.
  • src-tauri/src/window.rs — normal/desktop mode transitions, always-on-bottom, click-through, hide/show/toggle, status reporting, hotkey failure state.

4. Data flow

Boot

  1. Read settings.json (invalid values normalize to defaults).
  2. List board files, most recently updated first.
  3. Try the lastBoardId, then each remaining board.
  4. Load a board through the recovery chain (pending save, then main file, then previous backup). Recovery is surfaced in a toast.
  5. If no board can be opened, create a fresh board and keep the unreadable files in place. Nothing is deleted silently.
  6. Initialize Excalidraw from board.scene using toInitialData().

Editing and autosave

  1. Excalidraw onChange produces elements, app state, and binary files.
  2. sceneFromCanvas() builds a new board object. Only a deliberately chosen subset of app state is persisted: background color, scroll position, zoom, and binary files.
  3. A scene signature (element versions including soft-deleted tombstones + background + file sizes) avoids writing when nothing meaningful changed. Excalidraw's onChange reports elements including deleted tombstones, so the save path signs the same including-deleted set while persisting only live elements. Signing the visible-only set would make every deletion look dirty forever and could starve input during repeated flushes.
  4. createSaveQueue() debounces writes (700 ms), serializes overlapping saves, and reports saving, saved, or error status. A failed autosave remains dirty so a later flush can retry; it never retries in a tight loop.
  5. Explicit Ctrl+S, board switch/create/rename/duplicate/delete, settings changes, and window close flush pending work first.

The close path calls onCloseRequested, flushes the queue, and only then hides to the tray or quits. In the browser fallback, a pending save triggers the native beforeunload warning.

Save format

boards/<id>.underlay.json is versioned JSON. Legacy <id>.vanvas.json boards use the same schema and remain readable:

{
  "schemaVersion": 1,
  "id": "uuid",
  "name": "Board name",
  "createdAt": "ISO-8601",
  "updatedAt": "ISO-8601",
  "scene": {
    "elements": [],
    "appState": { "viewBackgroundColor": "#F7F3EA", "scrollX": 0, "scrollY": 0, "zoom": 1 },
    "files": {}
  }
}

Exports support the same Underlay format and an Excalidraw-compatible scene (type: "excalidraw") that can be imported into Excalidraw. PNG export is produced by the Excalidraw export API and written through a Tauri dialog.

5. Persistence, recovery, and data safety

Default packaged data root resolution:

  1. UNDERLAY_DATA_DIR, if set (legacy VANVAS_DATA_DIR is accepted).
  2. D:\Underlay, if D: exists. If only D:\Vanvas exists, it is reused in place so existing boards stay visible.
  3. %LOCALAPPDATA%\Underlay, or the pre-rename %LOCALAPPDATA%\Vanvas when only that folder exists.

Development and test runs override the root with <repo>/userdata or <repo>/userdata/desktop-e2e, so development boards never mix with a packaged installation.

Layout:

<root>/
  boards/<id>.underlay.json                    (legacy <id>.vanvas.json read in place)
  backups/<id>/previous.underlay.json          (legacy previous.vanvas.json read)
  backups/<id>/snapshot-YYYYMMDD-HHMMSS.json   (up to 8, at most every 10 min)
  trash/<id>-YYYYMMDD-HHMMSS-mmm.underlay.json
  logs/underlay.log                            (rotated at 2 MB)
  settings.json
  settings.previous.json
  exports/
  webview2/                                    (WebView2 profile)
  • Saves write a temporary file in the same directory and rename it over the main file, so a crash cannot leave a half-written board.
  • Before replacing an existing board, the current file is copied to previous.underlay.json (or the legacy previous.vanvas.json fallback). Periodic snapshots are pruned to the newest eight.
  • Loading prefers the newest valid pending temp file if it is at least as new as the main file, then the main file, then the previous backup.
  • IDs are validated against path traversal; names are trimmed and length-limited; board files are limited to 128 MB; settings are limited to 1 MB.
  • Deleting a board moves it to trash/ rather than erasing it.
  • Logging never records board contents; it records actions, sizes, and errors.
  • There is no automatic destructive cleanup. Trash and backups remain until the user removes them.

Schema evolution

schemaVersion is checked on load. A board from a newer schema is rejected with an explanatory error instead of being rewritten. Any future migration must copy the board and its previous backup before changing the format, and must document the migration and rollback path.

6. Desktop presentation layer

Window modes

  • Normal mode: decorated, resizable window (1280x820, minimum 900x600), normal taskbar presence, no global input interception.
  • Desktop canvas: the existing window is reparented under the desktop WorkerW, above the wallpaper and below icons. Canvas clicks and typing edit the board directly; icons, taskbar, and apps remain usable. Ctrl+Alt+X pauses canvas input, Ctrl+Alt+B returns to the window, and Ctrl+Alt+W toggles the canvas. This is the primary desktop presentation on supported builds.
  • Legacy desktop mode: undecorated, maximized within the current monitor's work area, always-on-bottom, taskbar remains usable. The webview is placed behind normal windows. The previous bounds/maximized state are saved and restored on exit. Retained as a fallback.
  • Click-through: available only in legacy desktop mode. Uses set_ignore_cursor_events(true), which sets WS_EX_TRANSPARENT | WS_EX_LAYERED on the window. It is disabled automatically when leaving desktop mode or hiding the window. This lets the user interact with icons/windows beneath the canvas. Keyboard focus is not captured by the transparent window.
  • Hidden: the process stays alive with a tray icon and global hotkeys.

Controls

Control Action
Ctrl+Alt+B Show/hide the canvas, or return from the desktop canvas
Ctrl+Alt+W Toggle the desktop canvas behind icons
Ctrl+Alt+X Pause/resume canvas input, or toggle legacy click-through
Ctrl+Alt+D Toggle legacy desktop mode
Ctrl+S Flush a save and confirm
Ctrl+, Open settings
Tray menu Show/hide, desktop canvas, legacy desktop mode, click-through, settings, quit
Settings Startup surface (window/tray/canvas), launch at login (off by default), close-to-tray

Launch at login is implemented with the Tauri autostart plugin and is only enabled by an explicit toggle. The autostart entry passes --autostart, which starts the process quietly in the tray, or attaches the desktop canvas directly when the startup surface setting is Canvas. Nothing is written to startup configuration at install or first launch.

Desktop canvas (Phase 1, implemented)

The app can reparent its existing Tauri window, including the WebView2 child, under the desktop WorkerW so the same canvas renders between the wallpaper and the desktop icons. There is no second engine, webview, renderer, or board store. Phase 0's isolated POC is documented in docs/DESKTOP_SURFACE_POC.md; Phase 1 is implemented in src-tauri/src/desktop_surface.rs.

Verified hierarchy on Windows build 26300:

  • Progman has WS_EX_NOREDIRECTIONBITMAP; SHELLDLL_DefView is a layered direct child; a WorkerW sibling sits below DefView. Classic GDI child painting and UpdateLayeredWindow do not composite on this raised desktop.
  • A plain WS_CHILD of WorkerW placed below DefView renders behind icons. The production canvas is the real Tauri window reparented into that slot, not a second surface.
  • attach saves and restores the original parent, style, rectangle, and zoom state. The normal window remains hidden while attached; detach shows it again.
  • A scoped WH_MOUSE_LL hook exists only while the canvas is attached and interactive. It passes non-canvas input to Windows, forwards canvas input to the WebView2 host, and treats a drag that starts on an icon as a shell drag. Icon rectangles come from read-only Shell COM (IFolderView2) on a worker thread. Alt+right-click is passed to Explorer for the real shell menu.
  • WebView2 cannot receive posted wheel input while DefView is above it, so the hook emits client coordinates and the page dispatches a synthetic wheel event for pan/zoom. Keyboard input uses a hidden 1×1 focus proxy that owns the foreground queue and forwards keys to the WebView2 host.
  • startupSurface selects normal window, tray, or desktop canvas on the next launch. --autostart stays quiet; --safe-mode disables the surface. Window destruction (including an Explorer restart) triggers a bounded rebuild and reattach, with the normal window as the failure fallback.
  • When no WorkerW exists, attach sends the undocumented 0x052C message once, bounded by a one-second SendMessageTimeoutW and a short settle wait. It is the only undocumented shell interaction and does not modify shell settings.

The legacy always-on-bottom window and click-through remain available as the fallback mode when the desktop canvas is unsupported, disabled, or detached.

Remaining limits: one monitor at 100% DPI was exercised; mixed DPI, monitor changes, sleep/lock, real Explorer restart, IME, touch/pen, and final release smoke were not verified in this build.

Deliberate limitations

  • The desktop canvas covers the primary monitor only. Desktop icons remain visible and interactive; apps and the taskbar stay above it. The legacy always-on-bottom desktop mode still covers icons visually and is retained only as a fallback.
  • The canvas reparents a single window to the primary display rectangle. Moving between monitors, mixed DPI, sleep/lock, and real Explorer restart recovery were not independently verified in the final build.
  • The canvas window is a child of WorkerW and cannot be foreground; keyboard input depends on the focus proxy acquiring the foreground queue. If Windows denies activation, the UI warns and the user can return to the normal window.
  • If WebView2 is missing, Windows may prompt to install it. The application does not install runtimes silently.
  • The tray menu itself was not automated; global hotkeys and settings were.

7. Security model

  • Tauri commands are explicitly registered; there is no shell execution, remote URL loading, or arbitrary filesystem API exposed to the webview.
  • Board IDs and names are validated; board paths are constructed from validated IDs. Import paths come from the OS file dialog and are size-checked.
  • Image export data is base64-decoded and written only to a user-selected save path.
  • The imported frontend is local. The packaged content security policy is currently the Tauri default (csp: null), which is documented as a hardening gap; there is no remote content, navigation, or network API in the app.
  • No secrets, credentials, telemetry, or analytics are present.
  • The app runs as the current user; it does not request elevation and does not modify machine-wide settings.
  • Launch-at-login, when enabled by the user, creates the normal per-user autostart entry through the Tauri plugin. Disabling the toggle removes it.

8. Verification architecture

  • Unit tests (Vitest + jsdom): scene conversion, app-state sanitization, settings normalization, filename safety, and the save queue's ordering, debouncing, error, and flush semantics. Canvas/FontFace probes from Excalidraw are stubbed only for module import in tests/setup.ts; the logic under test is not mocked.
  • Rust tests: atomic save/load round trips, path traversal rejection, name validation, backup recovery, interrupted-save recovery, trash, duplicate, Excalidraw import, settings round trip, storage info, and exports using real temporary directories.
  • Browser end-to-end: headless Microsoft Edge over the Chrome DevTools Protocol drives the actual Vite build in a browser, exercising drawing, sticky editing, highlighter, shortcuts, save status, export file contents (board and PNG), import round trip, board operations, theme, background, and error monitoring. Final result: 47 checks passed.
  • Desktop end-to-end: a real Tauri build is driven through WebView2's CDP endpoint while Win32 APIs verify window visibility and extended styles. It covers the Tauri store, disk writes, reload recovery, global hotkeys, desktop mode geometry and taskbar usability, click-through styles, close-to-tray save flushing, quiet --autostart, and uncaught webview exceptions. Final result: 22 checks passed. The harness detects a locked Windows session and substitutes Tauri commands for synthetic OS hotkey dispatch only while locked.
  • Release smoke: the embedded (non-dev-server) executable is launched against an isolated data root to confirm it serves packaged assets, includes the highlighter action, persists a board, and performs hide/show, desktop mode, and save operations. Final result: 11 checks passed.
  • Static checks and unit tests: TypeScript and ESLint are clean; 35 Vitest and 15 Rust tests pass. The desktop:surface lifecycle suite (attach, detach, window-loss recovery, safe mode, and an opt-in Explorer restart) is authored but has not been run, and the current release executable has not been smoke-tested. See docs/FINAL_REPORT.md for commands, artifacts, hashes, and disk figures.

9. Disk and environment

  • scripts/env.sh places TEMP, TMP, TMPDIR, npm cache/temp, CARGO_HOME, CARGO_TARGET_DIR, the WebView2 profile, and UNDERLAY_DATA_DIR in gitignored folders inside the repository.
  • Node and Rust are build-time prerequisites; the project does not install global packages or bundle toolchains.
  • build/tauri-target is the Cargo target directory.
  • No external models or datasets are downloaded. Excalidraw's bundled fonts and locales are copied from node_modules into public/excalidraw-assets at prebuild time (about 15 MB).
  • The Tauri NSIS bundler caches its packaging tools under %LOCALAPPDATA%\tauri\NSIS (about 7.9 MB). This is build-time-only; application and installer outputs go to build/.
  • Temporary test profiles and build temp files are created inside the repository and are gitignored.

10. Known technical debt

  • No CSP hardening yet (see section 7).
  • The bundled Excalidraw font files do not carry per-font license texts in the npm distribution used here; license texts and notices must be assembled and verified before any public redistribution.
  • Desktop mode covers one work area; multi-monitor behavior needs a dedicated test on a multi-display machine.
  • Tray menu interaction and the actual autostart registry entry were not exercised to avoid changing system state. The settings toggle, default-off state, and --autostart quiet launch were verified.
  • Rotation, clipboard text/image paste, and image insertion rely on Excalidraw and were not individually automated.
  • An unsigned NSIS installer was rebuilt under the Underlay identity after the desktop-canvas changes, but neither the renamed executable nor the installer was smoke-tested, and the installer was not installed on a clean machine. Packaging status is recorded in docs/FINAL_REPORT.md.