# ADR 005: Client packaging: PWA first, Capacitor for stores, Tauri for desktop

> One Vite-built web client behind a small platform adapter (storage driver, keyboard insets, haptics, share, files, deep links, lifecycle). - v1 ships as an installable PWA (service worker,…

Source: https://nooklet.danielalder.cz/decisions/005-mobile-and-packaging

Date: 2026-09-10. Status: accepted.

## Decision

- One Vite-built web client behind a small `platform` adapter (storage driver, keyboard insets,
  haptics, share, files, deep links, lifecycle).
- v1 ships as an installable PWA (service worker, OPFS-backed SQLite, `persist()`, badging).
- v1.x wraps the same build with Capacitor 8 for iOS/Android store apps, which adds native SQLite
  (no quota or eviction), exact keyboard height, share-sheet receiving, `nooklet://` scheme for
  Shortcuts/Siri, and home-screen quick actions.
- Desktop gets Tauri 2 later for a global quick-capture hotkey, tray, and deep links. Electron,
  React Native, and Flutter are out.
- Native-feel rules for the web client: fixed app shell with a single inner scroll container,
  `viewport-fit=cover` and safe-area insets, a keyboard toolbar positioned by a measured `--kb`
  variable (never `bottom: 0`), one moving editor element re-parented between blocks so focus
  transfers inside trusted events, swipe to indent/outdent, long-press drag from the bullet.
- The local database on a phone is a cache plus an outbox; the server is the truth. Pending ops
  are flushed on pause/resume/online, never left to Background Sync.

## Why

The user wants the app to work the way native apps do without maintaining a second UI. iOS
storage for web apps is best-effort (separate containers per icon, eviction under pressure),
which makes "server is truth" mandatory anyway. Logseq's own mobile uses this exact Capacitor
configuration (`KeyboardResize.None` with a CSS keyboard-height variable).

## Note: why the UI framework choice (ADR 006, SolidJS) does not lock us out of native shells

PWA, Capacitor, and Tauri all work by embedding a system or bundled webview and pointing it at
the ordinary web build; Electron would work the same way. SolidJS compiles to standard DOM
operations, so the identical client code runs unchanged inside any of them — only the
`platform` adapter's implementation swaps per target. This is exactly why Electron, React
Native, and Flutter are excluded above for different reasons than "wrong framework": Electron is
simply redundant with Tauri (same webview-wrapping approach, much larger binary), while React
Native and Flutter replace the DOM with a native-widget tree and would require rewriting every
UI component from scratch, regardless of which web framework we had chosen. A future native
rewrite is a decision about the widget model, not about SolidJS.
