Staging — blog preview only.
Skip to content

Same vault UI in browser, PWA, and desktop shells

Scheduled

8 min read By NT²

Unlock, list, search, and settings should not fork into three products. NT² Vault keeps one vault UI and lets each shell own only what the platform must own: how durable files live on that device.

Same vault UI in browser, PWA, and desktop shells

The claim is blunt: NT² Vault runs one vault UI across the browser tab, the installed PWA, and desktop and mobile shells that embed that UI in a system WebView.

That is not “write once, pretend it is native everywhere.” It is a product boundary. The vault experience—create, unlock, page the list, open an item, attach a file, lock again—belongs to one application. Shells differ where platforms force them to differ: how a durable database file and attachment ciphertext sit on disk, and which OS capabilities (biometrics, share sheet, window lifecycle) the web layer can call through a thin bridge.

If you open se.nt2.me in a tab, install it as a PWA, or launch a packaged desktop or mobile shell, you should recognize the same vault. Not a cousin with a rewritten settings tree. The same vault.

The constraint: three UIs means three security products

A structured, zero-knowledge vault is not a brochure site wrapped in a frame. Unlock derives keys in the client. Session keys live in memory and must clear on idle, refresh, and lock. Item payloads and attachment bytes are sealed before they touch durable storage. The list is paged from a real SQLite database, not hydrated as a giant in-memory array. Multi-tab behavior elects a single Writer. Those rules are entangled with the UI: every button that saves, searches, or attaches is a path into crypto and storage.

Teams often “solve” cross-platform by forking the surface.

  • A Swift or Kotlin app for phones.
  • An Electron or Qt tree for desktop.
  • A separate web client for the browser.

Each fork starts with good intentions—native widgets, store guidelines, “feels at home on the platform.” The cost lands later. Encryption semantics drift. Unlock edge cases get fixed on one shell and forgotten on another. Attachment chunking, envelope unwrap, and fail-closed password verification get three implementations and three bug histories. Marketing promises “the same vault on every device.” Engineering delivers three products that happen to share a logo.

There is a softer trap: keep one UI for the browser, then ship a “native” shell that reimplements only the screens that look hard—unlock and the list—while leaving settings, backup, and share flows on a webview half-path. Users notice the seam. Reviewers notice the seam. Security reviewers notice that the crypto boundary moved when the chrome changed.

The constraint is therefore not “WebViews are prettier than native UI.” It is that a vault’s trust model is the product. Duplicating the UI tree duplicates the attack surface and the regression surface. For a local-first vault that already bets on the web platform for crypto and storage in the browser, the honest move is to keep that bet and wrap it—not to rebuild it three times.

The design: one vault app, three shells, two storage shapes

NT² Vault’s primary client is a single web vault application. In the browser and as an installed PWA it is that app directly. On desktop and mobile shells, the same build runs inside a system WebView. Routes for entry, unlock, create, restore, dashboard, inbox, and settings stay shared. Svelte components stay shared. Domain rules for encrypt, decrypt, page, and attach stay shared.

What changes is the storage adapter under the same contracts.

In the browser (tab or PWA), the vault’s SQLite database and attachment ciphertext live in the Origin Private File System (OPFS): a real per-vault database file beside encrypted attachment frames. IndexedDB remains a small device-level index for the vault picker—not the relational attic. SQLite itself runs as WebAssembly in a dedicated Web Worker so the UI thread stays free for scrolling and typing.

On desktop and mobile shells, the same logical layout uses native files under the app’s private data directory: a per-vault SQLite file and a parallel attachments directory of encrypted .bin frames. Metadata still lives in SQLite. Ciphertext still lives outside the database. The product rule does not change when the host filesystem does.

flowchart TB
  subgraph shells [Shells]
    Tab[Browser tab]
    PWA[Installed PWA]
    Desk[Desktop shell]
    Mob[Mobile shell]
  end

  UI[One vault UI]

  subgraph storage [Storage adapters]
    OPFS[OPFS: vault.sqlite + attachment frames]
    Native[Native files: vault DB + attachment frames]
  end

  Tab --> UI
  PWA --> UI
  Desk --> UI
  Mob --> UI
  UI --> OPFS
  UI --> Native

Shells also expose a small bridge for capabilities the open web cannot own alone: biometric prompts where the OS allows them, system share sheets, haptics, and window lifecycle hooks that reinforce lock-on-blur or lock-on-close policies. That bridge is intentionally thin. It does not reimplement the vault. It does not hold the master password. It does not become a second crypto stack.

The result is three hosts and one product:

HostWhat the user seesWhere durable vault bytes live
Browser tabSame vault UIOPFS database + attachment files
Installed PWASame vault UI, installable chromeSame OPFS paths
Desktop / mobile shellSame vault UI in a WebViewNative per-vault files

Optional cloud sync, when Premium is enabled, stays a replica path on top of that local truth. Shell choice does not change zero-knowledge rules: the edge still sees ciphertext and account metadata, not plaintext payloads or KDF salt. Unlock remains local whether you are in a tab or a packaged app.

The trade-off: one UI means living with WebView limits

Shipping one vault UI through WebViews is a trade, not a slogan.

Native chrome is not free. Platform guidelines still matter for packaging, permissions, and store review. Biometrics and share sheets need careful bridging. Some OS widgets will never look identical to a fully native list. Engineers who want a SwiftUI-first redesign of every screen will find the architecture stubborn on purpose.

Capability matrices are real. OPFS availability, Worker behavior, and filesystem quotas differ across browsers. Desktop and mobile shells inherit WebView quirks of their host OS. Feature detection belongs at the storage and capability boundary—not as a quiet fork that invents a second unlock path “just for this platform.”

Message latency and adapter discipline stay. The browser path still pays for Worker RPC around SQLite. Shells still need adapters that fulfill the same open / read / write / stream contracts for attachment ciphertext. Convenience shortcuts—“just dump the blob into SQLite on mobile”—would collapse the metadata-vs-ciphertext split that keeps the relational engine fast.

Release cadence couples shells to the vault app. A bugfix in unlock lands everywhere the UI ships. That is the point. It is also a discipline: shell releases must track the vault build rather than drifting into private UI forks “until next quarter.”

We accept those costs because the alternative—three UIs—costs trust. A vault that unlocks differently on phone and desktop is not “platform native.” It is unfinished cryptography with better icons.

What we get back is a single place to reason about session memory, envelope encryption, paged lists, and attachment BlobStore rules. Reviewers can ask one question—“does this UI path preserve the local-first boundary?”—instead of three. Users can move from a browser day to a desktop day without learning a second product.

What we refuse

Architecture is clearer when the refusals are explicit.

We refuse a parallel native UI tree for the vault. Desktop and mobile shells embed the vault app. They may add thin OS bridges. They do not become a second settings, list, and unlock implementation.

We refuse different crypto stacks per shell. PBKDF2 derivation, AES-GCM envelopes, non-extractable keys, and fail-closed local verification are client rules everywhere. A shell does not get a “simpler” unlock that weakens those rules for convenience.

We refuse collapsing attachment ciphertext into SQLite “because native files are annoying.” Metadata stays queryable in the database. Encrypted frames stay in a file BlobStore—OPFS in the browser, native files on shells—so large binaries do not turn every list query into a warehouse scan.

We refuse making the shell the source of truth. The device’s vault database and attachment store remain authoritative for daily use. Cloud is optional replica transport. Packaging an app does not mean the server suddenly owns unlock.

We refuse silent product forks disguised as progressive enhancement. If a platform cannot meet the storage or Worker requirements for a safe local vault, that is a support boundary—not a permission to invent a weaker main-thread or server-backed variant behind the same brand.

These refusals protect the claim in the title. Same UI is not a build slogan. It is how one trust model survives three hosts.

One product, three doors

A local-first vault should feel like the same private attic whether you open it from a tab, pin it as a PWA, or launch a desktop or mobile shell. The attic’s furniture—routes, list behavior, encryption, lock hygiene—should not rearrange itself when the doorframe changes. Only the floorboards should: OPFS in the browser, native files where the shell provides a private data directory.

That story sits beside the rest of the local-first stack. For why the product can live without a heavy server on the critical path, read Why host a heavy server when a PWA can do everything locally?. For the browser database file boundary, see why vault SQLite lives in OPFS, not IndexedDB. For why attachment ciphertext stays outside the relational engine, see metadata in SQLite, ciphertext in BlobStore. For keeping that engine off the UI thread in the browser, see SQLite in a Web Worker is not optional.

If that model fits how you want a private vault to work across devices, you can try NT² Vault or read more at nt2.me.

Last updated 2026-10-07

Related stories