Files
SRCmail/docs/OFFLINE-CLIENT-ARCHITECTURE.md
Bernd RodlerandClaude Opus 5 b6fdfe72ca chore: housekeeping — rescue orphaned doc, ignore .DS_Store, adopt vnc-v0.3.0
Commits the offline-client architecture analysis doc that was sitting
untracked in docs/ — its own header already warns this exact thing
happened once before (~/vncmail-plus is a shared checkout; an earlier
untracked copy was lost to a concurrent branch switch). Confirmed the
hazard is still live: vnc/VNC-CHANGES.md itself was found deleted from
disk mid-edit by this session, by something else touching the checkout
concurrently, and had to be restored with `git checkout --` before this
commit. Committing on sight is the only defense against that, not a
process improvement for later.

Also:
- .DS_Store added to .gitignore (was untracked in docs/)
- introduces a VNC-side feature version, separate from package.json's
  upstream-tracking version (1.7.8, must stay that way per the fork's own
  rule 4 - bumping it would turn merging upstream releases into a diffing
  exercise). Retroactively bucketed at the milestone boundaries the commit
  history already has: v0.1.0 fork bootstrap, v0.2.0 S/MIME plugin
  audit+fixes, v0.3.0 the internal-CA foundation just landed. Tagged
  vnc-v0.3.0 on this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:10:03 +02:00

152 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bulwark / VNCmail+ — Offline & Native Client Architecture Analysis
Date: 2026-08-04 (updated same day — see §7 for a strategy-changing discovery)
Scope: what Bulwark (upstream `bulwarkmail/webmail`, forked as VNCmail+) delivers today for
offline use, and what has to be built to ship Electron (desktop), Capacitor/React-Native
iOS (IPA) and Android (APK) clients with local notifications, an encrypted local search index
(SQLite/SQLCipher), and true offline mail.
> **Note on this file's persistence:** `~/vncmail-plus` is a shared checkout — other sessions
> actively commit and switch branches here. An earlier untracked copy of this doc was lost to a
> branch switch. Commit this file (or move it somewhere durable) if you want it to survive.
## 1. Current state of the webmail repo (verified against ~/vncmail-plus source)
| Area | Status today | Evidence |
|---|---|---|
| Service worker | Installed, but **caches nothing**`fetch` handler is a deliberate no-op so the app is never usable offline | `public/sw.js:6-8,36` |
| Web app manifest | Present, installable PWA (icons, `protocol_handlers` for mailto/webcal) | `app/manifest.ts` |
| Push notifications | **Real** Web Push: VAPID subscribe, `Notification.requestPermission`, SW `push`/`notificationclick` handlers, relayed through an external push relay + a preview API route | `lib/web-push.ts`, `public/sw.js:38-42`, `app/api/push/preview/route.ts` |
| Local mail cache | **None.** IndexedDB is used only for plugin/theme blobs; `localStorage` only holds device IDs and Zustand UI-state (`persist()`), never message bodies | `lib/plugin-storage.ts`, `stores/account-store.ts:220` |
| Search | Server-side JMAP `Email/query` only, no client index | `lib/jmap/search-utils.ts` |
| Local encryption | None for cached data. The one AES-256-GCM routine (`lib/auth/crypto.ts`) encrypts the **session cookie server-side** using `node:crypto` — unusable in a browser/WebView | `lib/auth/crypto.ts` |
| Mobile/desktop packaging in *this* repo | **Nothing exists**: no `capacitor.config.ts`, no Electron main/`electron-builder`, no Tauri, no fastlane/gradle/Xcode | confirmed via repo-wide `find`; `.github/workflows/*` |
| JMAP client portability | `lib/jmap/client.ts` is pure `fetch()`, no Node-only APIs — portable into a WebView/Electron renderer unchanged | grep for `node:`/`require(` in `lib/jmap/*` = zero hits |
**Multi-account scope: confirmed YES** — the offline cache must support multiple simultaneous
Stalwart accounts per device (matches the webmail's existing `account-registry` store). This
multiplies SQLCipher key-management work (§3/§7): one isolated key per account, not one global key.
## 2. The fork in the road: shell strategy
**Option A — Native shell over a remote WebView.** Capacitor/Electron just point at the hosted
Bulwark URL. Cheapest, ships an APK/IPA/desktop binary fast, gets native push — but is *not*
offline.
**Option B — True offline-first client.** The client authenticates and syncs JMAP data
directly, keeps a local encrypted store, and only needs connectivity to *sync*, not to *read*.
Recommendation stands: **Electron first (Option-B-lite is nearly free there — see §4)**, mobile
starts with Option A, then graduates to Option B — **but see §7: for mobile, "graduate to
Option B" likely means extending an existing app, not building one from scratch.**
## 3. Build-vs-buy matrix (webmail-repo-only view — see §7 for the revised mobile view)
| Component | Off-the-shelf | What you build yourselves |
|---|---|---|
| Capacitor shell (iOS/Android project scaffolding) | Capacitor CLI generates both native projects | Splash/icons, deep-link config, `capacitor.config.ts` tuning |
| Local SQLite | `@capacitor-community/sqlite` — ships **native SQLCipher support** on iOS/Android; web fallback via `jeep-sqlite`/`wa-sqlite` | Schema, JMAP→SQLite mapping, migrations |
| SQLCipher key lifecycle | Native Keychain/Keystore APIs (via Capacitor Secure Storage) store the raw key | Key derivation/rotation, **per-account keys** (multi-account confirmed §1), wipe-on-logout |
| Full-text search | SQLite FTS5 ships free with SQLite | Tokenizer choice, incremental indexer fed by the sync engine |
| Native push | `@capacitor/push-notifications` wraps FCM/APNs | Relay extension, device-token registration, notification-tap deep-linking |
| Electron desktop | `electron-builder`; Electron's own cross-platform `Notification` API | Main process booting the existing standalone Next.js server; auto-update wiring |
| Background sync | iOS `BGTaskScheduler`, Android `WorkManager` | The actual poll/backoff/delta-fetch job logic |
| Biometric app-lock | `capacitor-native-biometric` | UI/UX, fallback-to-passcode flow |
| Store release pipeline | Fastlane/EAS-style CI, Apple/Google developer accounts | Signing config, CI secrets, store metadata |
## 4. Why Electron is the cheap win
Electron has no server-dependency problem: bundle the standalone Next.js server (the same
artifact the `Dockerfile` already produces) inside Electron's Node runtime, open a
`BrowserWindow` against `localhost`. Reuses 100% of the existing app including `app/api/**`.
Native `Notification` API replaces Web Push entirely on desktop. Ships well before mobile
Option B.
## 5. Phased roadmap
1. **Fix the service worker** — today's SW intentionally caches nothing (`sw.js:36`). Add
Workbox-style precaching of the app shell/static assets. Cheap, immediate PWA-offline-shell
improvement, no architecture change.
2. **Electron desktop** (§4) — bundle standalone server + BrowserWindow + native Notification +
`electron-builder` packaging.
3. **Capacitor mobile, Option A (remote shell)** — WebView on the hosted instance, native push
registration bridged into the relay, biometric app-lock. Ships an installable APK/IPA fast;
not offline yet. **Revisit against §7 before starting — extending `vncmail-native` may replace
this step entirely rather than complement it.**
4. **JMAP sync engine + SQLite/SQLCipher store** — design delta-sync via
`Email/changes`/`Mailbox/changes`; local schema, one key per account (§1); move auth off the
Next-only encrypted cookie into secure storage so mobile can talk to Stalwart directly.
**§7: `vncmail-native` already has a cruder version of the "local cache" half of this
(bulk AsyncStorage download) — the delta-sync/SQLite/SQLCipher/FTS half is still greenfield
there too, but auth/JMAP wiring is not.**
5. **FTS index + offline compose/outbox** — SQLite FTS5 population job; offline-composed
messages queued and replayed via JMAP `Email/set` on reconnect; conflict handling.
6. **Platform hardening** — background refresh scheduling, Apple export-compliance declaration
(SQLCipher/AES in the binary triggers `ITSAppUsesNonExemptEncryption`), signing/release CI.
## 6. Open questions — status
- ~~Does the referenced upstream React Native app already solve native push/device-pairing?~~
**RESOLVED — see §7.**
- **Is Bulwark upstream planning native clients?** Partially answered by §7: yes, `bulwarkmail/native`
is that plan, already public, beta/WIP. Still worth watching its upstream activity before
diverging further, since pulling upstream improvements is cheaper than re-diverging an AGPL fork.
- **Multi-account scope** — RESOLVED, see §1.
## 7. 2026-08-04 discovery: an upstream React Native app already exists — re-scope Phase 2
`bulwarkmail/native` (public, AGPL-3.0-only, Expo SDK 54, beta/WIP) is a React Native mobile
client for Bulwark. **Forked to `brvncde-dotcom/vncmail-native`.** It already ships:
- **Multi-account** JMAP sign-in against any server (e.g. Stalwart).
- **QR-code cross-device pairing** — `src/screens/LoginScreen.tsx` + `QrScanModal` +
`redeemPairingCode`/OAuth handoff in `src/lib/oauth.ts`. This is the "QR-code SSO login and
device pairing" feature referenced in the webmail's `CHANGELOG.md:207`.
- **Android push notifications via FCM**, dispatched through a *second* public upstream repo,
`bulwarkmail/relay` (also AGPL-3.0) — **this is the actual service behind the webmail's
`DEFAULT_RELAY_BASE_URL`**, resolving the open question from the original Phase-2 plan about
where that relay's source lives. It terminates JMAP `PushSubscription` pushes and forwards to
FCM (mobile) or Web Push (PWA); a single Bulwark-hosted instance serves every opted-in client
so self-hosters don't need their own Firebase project — **or you can self-host it** (Docker
compose provided) if you want push traffic to never leave VNC infrastructure. **Decided:
self-host.** Forked to `brvncde-dotcom/vncmail-relay`. Remaining: dedicated Firebase project
for FCM credentials, a VAPID keypair, a microk8s deploy alongside `vncmail-plus` (per
`vnclagoon-suite-microfrontends`), and repointing both `vncmail-plus`
(`DEFAULT_RELAY_BASE_URL`) and `vncmail-native` at the self-hosted instance. Sequenced in the
`VNCprodbuild` skill's Phase 2 step 0.
- **A basic offline mail cache already**: `src/lib/offline-sync.ts` (155 lines) bulk-downloads
the last N days of mail via `Email/query`+`Email/get` into `src/stores/offline-cache-store.ts`
(AsyncStorage-backed, size-capped, evicts oldest), with live progress UI
(`OfflineCacheBanner.tsx`). **This is not the delta-sync/SQLite/SQLCipher/FTS engine Phase 2
called for** — it's a periodic bulk re-download, not incremental `Email/changes` sync, and
storage is plain JSON in AsyncStorage, not an encrypted database — but auth, JMAP wiring, and
the UI shell around "offline mail" already exist.
- Android release pipeline (`.github/workflows/release-android.yml`, sideload APK from GitHub
Releases) and an iOS release pipeline (`release-ios.yml`, `docs/ios-release.md`, TestFlight)
**already exist** — iOS *builds*, just without push (see below).
**Still genuinely missing** (confirmed against its own README + source):
- iOS push notifications and client certs — Android-only so far.
- No SQLite/SQLCipher/FTS anywhere (`@react-native-async-storage/async-storage` +
`expo-secure-store` only) — the encrypted-local-index work is still fully greenfield.
**Resolved 2026-08-04:** use `expo-sqlite`'s official `useSQLCipher` config-plugin option
(Android/iOS/macOS) rather than a third-party binding. Unusable in Expo Go, so this forces a
custom dev client for development going forward — accepted. Stay Continuous-Native-Generation
(don't commit `ios`/`android`, let `expo prebuild` regenerate them) rather than going fully
bare, since a committed native tree would conflict on every future merge from upstream
`bulwarkmail/native`. Detail in the `VNCprodbuild` skill's status log.
- Filters & rules, S/MIME, plugins, themes, file storage are UI stubs only.
- No Play Store distribution yet.
**Strategic implication:** for the mobile leg of the native-client roadmap, **extending
`vncmail-native` is very likely cheaper than building a Capacitor wrapper around the webmail
from scratch** — it already has the parts that were the most speculative/decision-heavy in the
original Phase-2 plan (auth, pairing, push wiring, multi-account, a working offline-mail UX
shell). The remaining work narrows to: iOS push, replacing the AsyncStorage bulk-cache with a
real `Email/changes` delta-sync engine into SQLite/SQLCipher, an FTS5 index, and an
offline-compose/outbox queue — i.e., roughly roadmap steps 46 above, now scoped against an
existing app instead of a blank one. **This should be a formal decision gate before touching
Phase 2 further**: adopt `vncmail-native` as the mobile client going forward (dropping/deferring
the Capacitor-wraps-webmail plan for mobile), or keep both in parallel. Recommend adopting it —
duplicating auth/pairing/push work that already exists and works has no upside.