# Car Parts Price Comparator — Open-Sourcing & Distribution Models

**Status:** Draft v1 · 2026-07-14
**Companion documents:** `MONETIZATION.md` (economics; §7.4 names the "free public utility" liability this document partially answers), `WALLED-STORES.md` (the remote-browser subsystem that distribution models §3–§4 would replace per-user), `TECHNICAL-SPEC.md` (core meta-search).
**Scope:** Whether and how to open-source the project, and which distribution model to ship — hosted service, desktop app, or mobile app. This is a strategy/design document; nothing here is built or committed.

> **Supersession note (2026-07-16):** The measurement/CTR pipeline and opt-in telemetry alternatives discussed below were removed from product scope. Search history is user-visible persistence, and no replacement telemetry is planned.

---

## 1. Open-sourcing: what it changes, and what it doesn't

The decision to publish the code is **largely independent of the legal analysis** already done in `MONETIZATION.md`. The legal exposure was never in the code — it is in *operating a service*. Publishing the repository neither launders that exposure nor meaningfully adds to it.

### 1.1 What does not change

Whoever **runs** a deployed instance faces the same analysis as before:

- **EU Database Directive / sui generis right.** The uncomfortable precedent is *Innoweb v Wegener* (CJEU C-202/13): a dedicated meta-search engine over car listings was held to "re-utilise a substantial part" of the underlying database. That is exactly this product's shape. The mitigating argument is *British Horseracing Board v William Hill* (C-203/02): a store's catalog of **its own products at its own prices** is *created* data, not *obtained* data, and likely fails the Directive's "substantial investment in obtaining" test. Both arguments attach to the operator, not the author.
- **Store terms of service** bind whoever accepted them (the operator/user), not the code publisher.
- **Latvian unfair-competition angles** — again, operator-side.

None of this turns on whether the repo is public.

### 1.2 What does change

- **Author liability is close to nil.** Publishing scraping code is commonplace and broadly protected — Scrapy, price trackers, and store-specific adapters exist all over GitHub. A standard OSS license's warranty disclaimer covers the author for what others do with the code.
- **Framing is the one real publication risk.** GitHub has taken down repos *marketed* as "Cloudflare bypass" tools. Our approach is not circumvention — a real headful browser passing CF's own managed challenge, with human-solve as the fallback (see `WALLED-STORES.md` §1 and the standing no-evasion policy) — but a README that leads with "how we get past Cloudflare" invites both a takedown report and a hostile reading. **Describe the project as a price comparator with a browser-based fetch layer. Document the architecture, not the walls.**
- **Visibility cuts against us operationally.** A public repo enumerating each store's endpoints (`api.carparts.lv/article/searcharticlenr`, eoltas `/rest/product-price`, the ic24 session dance) makes it trivial for stores to find and block the tool. Not a legal problem — an adapter-rot accelerant. Expect faster breakage once public.

### 1.3 Pre-publication checklist

1. **License.** MIT or Apache-2.0 for maximum reach; **AGPL-3.0** if a closed commercial clone of a hosted version would be unacceptable. Decision open.
2. **Scrub the full git history** for secrets before the repo goes public: affiliate credentials, session tokens, personal infra details in `ops/`, dev-box specifics. Anything found requires a history rewrite (`git filter-repo`), not just a new commit.
3. **README posture:** price-comparison tool; users are responsible for complying with target sites' terms; conservative default rate limits; no bypass language.
4. **Separate the framework from the adapters.** The store-agnostic core (adapter interface, variant grouping, BrowserHub, Angular frontend) is the reusable artifact. The ten Latvian adapters are the fragile, attention-drawing part. Keeping them a cleanly separated layer (or a separate repo) preserves the option to react if one store objects, without touching the core.

---

## 2. Distribution models compared

The current form — Node backend + Docker remote-browser on a home box — is operable only by its author. Three candidate models for making it usable by others:

| | Hosted service (+ PWA) | **Desktop app (Electron)** | Mobile app (Capacitor) |
|---|---|---|---|
| Reuses existing code | Everything | Everything (thin shell) | Frontend yes; adapter fetch layer must be ported |
| Who fetches from stores | **Our server** (central operator) | User's machine, residential IP | User's phone, carrier IP |
| CF-walled stores | Needs the whole `WALLED-STORES.md` browser farm, per *our* IP | **Solved by construction** (§3) | Solved via hidden WebView (same principle, new implementation) |
| Legal posture | *Innoweb*-shaped: we operate a meta-search service | Each user runs their own lookups; we distribute a tool | Same as desktop |
| Standing cost to us | Infra + the C&D tail risk (`MONETIZATION.md` §7.4) | ~zero (GitHub Releases) | ~zero (Android); $99/yr + review friction (iOS) |
| Measurement (M3 CTR) | Works as built | **Lost** unless opt-in telemetry (§5) | Same as desktop |
| Effort from here | Small (already runs) | Days | Weeks (after the §4 refactor) |

A browser extension was considered and dismissed: sessions already live in the user's browser, but Manifest V3 fights cross-origin fetching the whole way, and the backend would need a rewrite anyway.

**Recommendation: Electron first, Android second, iOS on demand.** The hosted model is not a distribution strategy — it is the operator role we already occupy, with all its liabilities; it remains the right form only for the private POC and measurement phase.

---

## 3. The key insight: the user's machine *is* the warm headful browser

The M5 finding was that Cloudflare's **managed** challenges pass **non-interactively** in a real, warm, headful browser on a residential IP — the entire reason `WALLED-STORES.md` builds a Dockerized headful Chrome + Xvfb + noVNC on a home box is to give *our server* those properties.

In a desktop distribution, **every user's machine has those properties natively**: a residential IP running real Chromium (which is what Electron ships). The walled-store subsystem stops being infrastructure and becomes a code path:

- **BrowserHub gets a second implementation** backed by hidden Electron `WebContents` instead of the remote Docker browser. The adapters are already store-agnostic behind the BrowserHub interface (`fetchJson` / `fetchPageHtml` / `fetchOriginHtml`), so this is a swap, not a rewrite.
- **Session persistence** (`session.fromPartition('persist:trodo')` etc.) keeps `cf_clearance` cookies warm across app launches — the "warm browser" property for free.
- **`needs_solve` becomes a native UX** instead of noVNC: make the hidden window *visible*, the user clicks the checkbox in a real window, hide it again. This is strictly better than the remote-browser flow it replaces — and it remains squarely inside the no-evasion policy: a real human solving a real challenge in a real browser, now simply *their own*.

**Open risk to verify first:** Electron's Chromium is not branded Chrome (missing codecs/DRM, distinct UA surface), and CF's fingerprinting *might* care. The cheapest experiment: a WebContents-backed fetch against one walled store (trodo's JSON API) before committing to the model. This is the go/no-go gate for the whole desktop plan.

On mobile the same principle holds with different machinery: a hidden **WKWebView / Android WebView** is a real browser engine on a **carrier IP** (CGNAT, generally treated as gently as residential), and `needs_solve` becomes a modal WebView the user taps through.

### 3.1 Electron shell — shape of the build

> **Superseded (2026-07-15):** the "backend in-process on localhost" sketch below was replaced during implementation planning by a serverless design — the engine runs in Electron's main process behind an IPC gateway, with no HTTP server and no SQLite in the shipped app. See `STANDALONE-APP-IMPLEMENTATION-PLAN.md` §1.1. The packaging/tradeoff points below still stand.

- ~~Keep the Fastify backend as-is, run it inside Electron (main process or a `utilityProcess`) bound to `localhost` on a random port; point the BrowserWindow at the already-built Angular app in `public/`. No IPC refactor, no API changes — the app is a thin shell around what exists.~~
- `electron-builder` produces the target artifacts: **NSIS installer + portable .exe** (Windows), AppImage (Linux), dmg (macOS). Auto-update via `electron-updater` + GitHub Releases — free for an open-source repo.
- Accepted tradeoff: ~90–120 MB installer, because Chromium comes along. Tauri would be ~10 MB but uses the *system* webview (WebView2 ≈ Chromium on Windows; WebKit elsewhere — not equivalent for CF purposes) and has no Node runtime, so the backend would need a sidecar or a Rust rewrite. **Chromium is a feature for this product, not dead weight; Electron's size is justified here in a way it usually isn't.**

### 3.2 Mobile — shape of the build

- **Capacitor** wraps the existing Angular frontend in a native shell. Phones don't run Node, so the backend cannot come along — the adapter layer runs on-device instead (prerequisite: §4).
- JSON-API stores (carparts, handler, eoltas, trodo) port near-trivially: Capacitor's native HTTP plugin replaces undici (bypasses CORS, speaks HTTP/2 natively — satisfying the HTTP/2-or-403 constraint from `TECHNICAL-SPEC.md` §1). HTML parsing uses the WebView's own `DOMParser` instead of cheerio.
- CF-walled HTML stores: hidden WebView per store, visible-modal fallback for `needs_solve`.
- **Distribution:** Android first — APK on GitHub Releases, F-Droid for the open-source audience, Play Store optional (price comparators scraping via WebView are unremarkable there). iOS deferred: Mac build machine, $99/yr, App Store review on every release; EU DMA sideloading exists but is practically niche. Ship Android, let iOS wait for demand.
- **Mobile-native differentiator for later:** camera OCR of the stamped part number — the moment mobile stops being "the desktop app, smaller" and becomes the obviously better product for the person standing at the car.

---

## 4. Prerequisite refactor: runtime-agnostic adapters

The move that makes desktop *and* mobile cheap is one the codebase is already halfway to (variant grouping is a pure, tested module): **split each adapter into a request descriptor and a pure response parser.**

- *Request descriptor:* URL, method, headers, body — plain data, no I/O.
- *Pure parser:* HTML/JSON string in → offers out. No fetch, no runtime dependency.
- The fetch itself is **injected**: undici (server), Electron WebContents (desktop), Capacitor HTTP / WebView (mobile).

Then one adapter per store serves all three runtimes, instead of three implementations rotting at three speeds. **This refactor should land before the Electron shell**, so Electron consumes the portable form from day one, and the existing fixture-based tests keep covering the parsers unchanged (they already test string-in/offers-out).

---

## 5. What the distributed models break: measurement

The M3 pipeline (`POST /api/searches` sid → `/out` clickout log → `/stats`) assumes a central server. Desktop and mobile users hit stores directly from their own machines; there is no `/out` hop to log.

If the ≥30% CTR gate still matters at distribution time, the options are:

1. **Opt-in telemetry** phoning home anonymous search/clickout counts — technically easy, but a deliberate privacy decision; for an open-source tool the default should be **off**.
2. **Accept the loss:** the gate was a POC-phase instrument to decide whether the product is worth building. By the time a distributable ships, that question is answered; GitHub stars/downloads become the (cruder) signal.

Leaning (2), with (1) held as an option if a monetization path (`MONETIZATION.md` §5) later needs audience numbers.

---

## 6. How this reframes the `MONETIZATION.md` §7.4 liability

§7.4's verdict was that a free *hosted* utility is a standing liability: infra cost + maintenance labour + operator-side legal tail, unfunded. The desktop/mobile distribution model dissolves two of the three:

- **Infra cost → ~zero.** No server, no browser farm; GitHub hosts the releases.
- **Operator legal tail → dispersed.** There is no central operator; each user performs their own handful of lookups from their own IP. No bulk extraction, no central catalog copy, no substitution effect against any store — the *Innoweb* shape disappears. What remains is author-side publication risk (§1.2), which is small and manageable by framing.
- **Maintenance labour → remains, and grows.** Adapter rot continues, accelerated by public endpoints (§1.2), and now ships on users' machines with an update-latency lag. This is the honest residual cost of the model, and auto-update (§3.1) is the mitigation, not a cure.

Open source + local-first distribution is therefore not just a philosophy choice — it is the version of "free public utility" that §7.4 could not have: one with **no standing bill and no operator to send a C&D to.**

---

## 7. Recommended sequence

1. **Adapter purity refactor** (§4) — pure parsers + injected fetch; existing fixtures keep passing.
2. **Electron go/no-go experiment** — WebContents fetch against trodo's JSON API from a residential IP; verifies the §3 assumption for days, not weeks, of effort.
3. **Electron shell** (§3.1) — backend in-process, `electron-builder`, portable .exe + installer artifacts.
4. **Pre-publication pass** (§1.3) — history scrub, license, README posture — then flip the repo public alongside the first desktop release.
5. **Android/Capacitor** (§3.2) — after desktop proves the model; measured in weeks, not a rewrite.
6. **iOS, camera OCR** — on demand, if the product earns it.
