# Native-only capabilities behind interfaces

The repository's architectural decision, rationale and consequences.

# Native-only capabilities behind interfaces

**Status:** accepted · **Date:** 2026-10-31 · **Related:** [ADR-0013](0013-mobile-features-mirror-the-backend-shape.md), [ADR-0019](0019-encrypted-local-store-on-the-device.md)

**Enforcement:** per-capability tests on the web variant verify the shape now; the native variant files throw `NativeOnlyError` so a native build cannot silently run against unimplemented backends. `scripts/mobile-feature-boundaries.test.mjs` keeps the `.native` / `.web` variant pattern inside a feature or `platform/` folder (never under `src/app/`).

The Expo app runs today in Expo web and will later run on iOS and Android. Several capabilities exist natively but not on the web (SecureStore, LocalAuthentication, SQLCipher, native camera, volume-key shutter, widgets), and several web APIs stand in for them during development. If each feature branches on `Platform.OS` inline, the web build silently degrades and the native build drifts from what we have tested; neither is acceptable for a clinical product.

## Decision [#decision]

**Each native-only capability lives behind a platform interface, with a web implementation verified now and a native variant filed under the same stem (`.native.ts`), replacing the throw-stub in a later ticket.** The initial set of interfaces (ticket 15):

| Capability         | Interface                             | Web (ticket 15)                                                                                               | Native (ticket 75)                                |
| ------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| Vault keystore     | `platform/vault/keystore.ts`          | record sealed under a non-extractable CryptoKey in IndexedDB (`createVaultKeystore`); PIN only, no biometrics | `expo-secure-store` + `expo-local-authentication` |
| Local store        | `platform/local-store/local-store.ts` | IndexedDB (`createIdbLocalStore`)                                                                             | `expo-sqlite` + SQLCipher                         |
| WebCrypto provider | `@wundklare/crypto` provider          | `globalThis.crypto.subtle`                                                                                    | `react-native-quick-crypto`                       |

The keystore's native variant (`keystore.native.ts`) exports the same factory as the base and currently throws `NativeOnlyError` (`platform/vault/native-only.ts`); the SQLCipher `LocalStore` arrives with ticket 75 as a new adapter behind the same interface. The throw references this ADR. Metro picks the `.native.ts` variant on iOS and Android automatically; the throw turns the capability into a loud build-time failure the first time a native build touches it, which is what we want until ticket 75 lands the real implementation.

## Consequences [#consequences]

- The web app ships complete now, with the same interfaces the native app will consume, so no feature code changes when the native ticket lands.
- A capability never silently degrades across platforms; the throw-stub keeps the gap visible.
- The web variant must be production-quality where it is the only variant we have shipped (vault keystore, IndexedDB store, PIN-only unlock). "Dev-only" shortcuts in the web implementation are not acceptable — they would mislead reviewers and users.

## Alternatives considered [#alternatives-considered]

- **Inline `Platform.OS` branches.** Rejected: the native branch is unreachable in test and will drift from the interface the web branch evolves to.
- **A single implementation that forces the native primitive onto web** (for example, polyfilled SQLCipher). Rejected: the carrying cost on web is high and the shared implementation locks us out of better native primitives (biometrics, hardware key).
- **A noop web implementation that defers everything to native.** Rejected: we need Expo web to work in dev, demos, review, and web-first internal tools.

