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.
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).
| 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. |
- 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.
+---------------------------------------------------------------+
| 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 |
+---------------------------------------------------------------+
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/—BoardStoreinterface plusTauriBoardStore(IPC) andBrowserBoardStore(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.
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.
- Read
settings.json(invalid values normalize to defaults). - List board files, most recently updated first.
- Try the
lastBoardId, then each remaining board. - Load a board through the recovery chain (pending save, then main file, then previous backup). Recovery is surfaced in a toast.
- If no board can be opened, create a fresh board and keep the unreadable files in place. Nothing is deleted silently.
- Initialize Excalidraw from
board.sceneusingtoInitialData().
- Excalidraw
onChangeproduces elements, app state, and binary files. sceneFromCanvas()builds a new board object. Only a deliberately chosen subset of app state is persisted: background color, scroll position, zoom, and binary files.- A scene signature (element versions including soft-deleted tombstones +
background + file sizes) avoids writing when nothing meaningful changed.
Excalidraw's
onChangereports 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. createSaveQueue()debounces writes (700 ms), serializes overlapping saves, and reportssaving,saved, orerrorstatus. A failed autosave remains dirty so a later flush can retry; it never retries in a tight loop.- 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.
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.
Default packaged data root resolution:
UNDERLAY_DATA_DIR, if set (legacyVANVAS_DATA_DIRis accepted).D:\Underlay, ifD:exists. If onlyD:\Vanvasexists, it is reused in place so existing boards stay visible.%LOCALAPPDATA%\Underlay, or the pre-rename%LOCALAPPDATA%\Vanvaswhen 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 legacyprevious.vanvas.jsonfallback). 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.
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.
- 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+Xpauses canvas input,Ctrl+Alt+Breturns to the window, andCtrl+Alt+Wtoggles 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 setsWS_EX_TRANSPARENT | WS_EX_LAYEREDon 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.
| 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.
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:
ProgmanhasWS_EX_NOREDIRECTIONBITMAP;SHELLDLL_DefViewis a layered direct child; aWorkerWsibling sits below DefView. Classic GDI child painting andUpdateLayeredWindowdo not composite on this raised desktop.- A plain
WS_CHILDof WorkerW placed below DefView renders behind icons. The production canvas is the real Tauri window reparented into that slot, not a second surface. attachsaves 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_LLhook 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.
startupSurfaceselects normal window, tray, or desktop canvas on the next launch.--autostartstays quiet;--safe-modedisables 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
0x052Cmessage once, bounded by a one-secondSendMessageTimeoutWand 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.
- 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.
- 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.
- 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:surfacelifecycle 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. Seedocs/FINAL_REPORT.mdfor commands, artifacts, hashes, and disk figures.
scripts/env.shplacesTEMP,TMP,TMPDIR, npm cache/temp,CARGO_HOME,CARGO_TARGET_DIR, the WebView2 profile, andUNDERLAY_DATA_DIRin gitignored folders inside the repository.- Node and Rust are build-time prerequisites; the project does not install global packages or bundle toolchains.
build/tauri-targetis the Cargo target directory.- No external models or datasets are downloaded. Excalidraw's bundled fonts and
locales are copied from
node_modulesintopublic/excalidraw-assetsat 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 tobuild/. - Temporary test profiles and build temp files are created inside the repository and are gitignored.
- 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
--autostartquiet 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.