# Adapter Sets — Design Plan

**Status:** Draft v2 · 2026-07-15 — v2 reverses v1's central choice: **OTA delivery of adapters as downloaded code is dropped** in favour of bundled sets + the app auto-updater. v1's OTA design is preserved in §7 as "considered and deferred," with the reasoning, so the decision is auditable and reversible at scale.
**Companion documents:** `STANDALONE-APP-IMPLEMENTATION-PLAN.md` (the engine/gateway split and the `electron-updater` auto-update this plan now relies on), `DISTRIBUTION.md` (multi-country expansion is the §7 growth lever), `MONETIZATION-PLAYBOOK.md` (the Phase 3 workshop SLA, which auto-update still satisfies — see §5), `TECHNICAL-SPEC.md` (`StoreAdapter` contract).
**Scope:** How adapters become country-grouped, user-selectable **sets**, and how store-breakage fixes reach users. The headline: sets are a build-time concept, fixes ship via normal app auto-update, and no adapter code is ever downloaded and executed outside the signed app binary.

---

## 1. The decision, and why it's this and not OTA

**Goal:** a Lithuanian user shouldn't wade through Latvian stores; and when a store breaks (which happens often — adapter rot is this project's defining maintenance cost), the fix should reach users quickly.

**v1 pursued this with over-the-air adapter packs** — signed code downloaded from GitHub and executed by the app, updatable independently of an app release. That works, but it turns adapters into *semi-trusted downloaded code*, which drags in a large, security-critical apparatus: a signing key with its own threat model, an isolated execution realm, a capability-scoped browser facade, an origin-allowlist enforced host-side, a CI manifest lint, and an engine-API surface promoted to a versioned public contract with backward-compat obligations. That is a lot of the highest-risk surface in the whole project, in service of one benefit: *fix-reaches-user latency*.

**v2's realization:** the app already ships an auto-updater (`electron-updater`, `STANDALONE-APP-IMPLEMENTATION-PLAN.md` Phase E). Auto-update *already delivers that one benefit* — a fix published to GitHub Releases reaches a user on their next launch — at the cost of a full-app download (free on Releases) and a restart. So OTA-of-code buys very little that auto-update doesn't, while costing the entire semi-trusted-code problem.

**Therefore (decision 2026-07-15):**

- **Adapters stay bundled, first-party, compiled into the signed binary** — inside the trust boundary, exactly as today.
- **Sets are a build-time grouping**; selection is pure UI over bundled adapters (§3).
- **Fixes ship as normal app versions via auto-update** (§5).
- **No adapter code is downloaded or executed outside the app binary.** The whole OTA apparatus (§7) is not built.

**The knock-on simplification — this is the big win, not just saved plumbing:** because adapters remain first-party compiled code, the browser-privilege problem that dominated v1 *disappears*. v1 needed a capability-scoped browser facade, realm isolation, and host-enforced allowlists **solely because** downloaded packs were semi-trusted. Bundled adapters are trusted, so they keep receiving the raw injected `FetchContext.browser` exactly as they do now. The signing-key threat model collapses to "sign the app," which the standalone plan does anyway. The single hardest part of v1 is deleted by this decision, not merely deferred.

## 2. What a "set" is

A **set = one country's stores** as a unit: `lv` = {partsale, xparts, eoltas, carparts, handler, trodo, rd24, euautodalas, autodoc, ic24}; a future `lt`, `ee` likewise. A set is the *selection* granularity (users think in countries, not stores) and the *mental* unit for maintenance ("the LV set has a broken store"), but it is **not** a separately shipped artifact — every set is compiled into every build.

Today `src/core/registry.ts` statically imports and lists all ten adapters. The change is small: tag each adapter with a `country` (or set id), and let the registry expose sets and honour a user's enabled-set selection. `enabled`/`fetchMode`/`getAdapter` semantics are unchanged; a store not in an enabled set is simply not queried.

## 3. Selection UX (bundled — zero download infrastructure)

- **First run / install:** detect locale as the default suggestion, show a set picker (multi-select — a border-town user may want LV + LT), persist the choice. Because all adapters are already present, "selecting" a set just adds its stores to the queried set — no download, no verification, no manifest.
- **Settings → Store sets:** add/remove countries anytime; instant, since nothing is fetched.
- **Default:** locale-matched set enabled; the user can widen or narrow.

This is strictly simpler than v1's pack picker — it's a preference toggle, not a download-and-verify flow.

## 4. Adapter grouping in the codebase

- Add a `country: string` (or `set: string`) field to `StoreAdapter` (`src/core/types.ts`), or a small set→store-ids map beside the registry. Lean: a field on the adapter, so a store's set membership lives with the store.
- `registry.ts` gains `setsAvailable()` and filters `enabledAdapters()` by the user's selected sets (selection passed in from config/UI state, not read as a singleton — consistent with the composition-root pattern).
- Fixtures, tests, and the `record`/`canary` scripts are unaffected — they already operate per adapter.
- Cross-country retailers (a pan-EU autodoc-class store, if the affiliate path ever opens) can belong to a synthetic `eu`/regional set or to multiple sets; a store's `country` can be a list if needed. Defer the shape until a second country is real.

## 5. How fixes reach users — auto-update, and the SLA

A broken store is fixed the same way any code change ships: land the adapter fix, cut a new app version, CI publishes to GitHub Releases, `electron-updater` delivers it.

- **Desktop:** app checks Releases on launch, downloads in the background, applies on restart. A workshop that opens the app daily gets a Tuesday-afternoon fix by Tuesday or Wednesday. This **satisfies the `MONETIZATION-PLAYBOOK.md` Phase 3 "fixes within 3 business days" SLA** without any OTA machinery — the reason OTA looked strategically load-bearing turns out to be covered here.
- **Android:** updates go through the Play Store (review latency, hours–day+) or F-Droid's build cadence, and the user accepts the update. Slower than desktop, but comfortably inside a 3-business-day SLA. This is the one place OTA had a genuine latency edge — and also the place OTA was hardest and riskiest (interpreted-code policy, likely baseline-only fallback), so trading it away is a good deal, not a wound.
- **Delta updates** (`electron-updater` supports them) keep the "full app for a one-line adapter fix" bandwidth cost modest; on free Releases hosting it's a non-issue regardless.

## 6. What this costs (stated honestly)

- **Every binary carries every country's adapters.** At 3 countries × ~10 small adapters this is kilobytes — irrelevant. It only starts to matter at large scale (see §7's reconsider trigger).
- **A fix is a full app version bump + auto-update**, not a sub-version hot-patch. For this product nobody needs finer granularity than "next launch."
- **Android fix latency is store-paced,** as above — accepted.

None of these is felt by the product at its actual scale. That's the whole case.

## 7. OTA — considered and deferred (the v1 design, and its reopen trigger)

Recorded so the reversal is auditable and the door is not bricked shut.

**What v1 proposed:** country adapter *packs* as signed JS modules, listed in a CI-generated manifest, served from GitHub Releases, downloaded and verified (signature + sha256 + origin pinning, fail-closed to a bundled baseline), executed in an isolated JS realm behind a capability-scoped browser facade bound to a per-adapter, manifest-declared, host-enforced origin allowlist; with unconditional host policy (localhost/LAN/metadata blocked, authenticated partition isolated) surviving even a validly-signed broad allowlist; gated by an engine-API compatibility contract; key custody via a CI-held signing key (protected GitHub Environment) with a trust-a-set-of-keys rotation scheme, Sigstore keyless as the growth path.

**Why it's deferred, not built:** every element above exists to make *downloaded code* safe and compatible. Auto-update (§5) delivers the one benefit — fix latency — that justified taking on that surface, so the cost/benefit inverts. The security analysis is sound and is retained here as the design that *would* be built if OTA were ever revived.

**What would reopen it:** scale. If the catalog grows to many countries / many dozens of stores such that (a) bundling every adapter into every binary becomes a real download-size or review burden, or (b) per-country fix cadence genuinely needs to outpace app-store review on a platform that matters, then per-country delivery earns its complexity and this section becomes the starting spec. Until then: **YAGNI.**

**One durable lesson to carry forward regardless:** the moment any code is executed that did not ship inside the reviewed, signed binary, the entire §7 apparatus (realm isolation as the load-bearing control, host-owned allowlists, unconditional target policy, key custody) becomes mandatory, not optional. Bundling is what lets the project *not* pay that — a property worth protecting deliberately.

## 8. Open questions

1. **Set membership shape:** a `country` field on `StoreAdapter` vs. a separate set→stores map; single-country vs. multi-country membership for pan-EU stores. Lean: field on the adapter, single-country until a real case forces otherwise. Low stakes, decided at build time.
2. **Default-selection heuristic:** locale only, or locale + a "select all nearby" convenience for border regions. Cosmetic; decide during the picker build.
3. (Deferred with §7) everything about pack signing, realms, and the engine-API contract — reopened only on the §7 scale trigger.
