Skip to content

Provider Plugins

Runa the Artificer — Backend Engineer

By the end of this chapter you’ll understand:

  • Why every provider plugin is declarative data only, and what that means for App Store safety
  • How a plugin travels from Cloudflare R2 through signature verification and schema validation to activation
  • How the bundled scraper engine uses config fields — not downloaded code — to navigate a bank’s login page and extract transactions

The core idea

Every bank integration used to live inside the app binary. To add SEB or fix a broken AMEX parser, you’d rebuild, TestFlight, App Store review, wait. The declarative plugin system moves that knowledge out of the binary into signed data bundles on Cloudflare. The app downloads a bundle, checks its Ed25519 signature against a pinned trust root, validates its schema, and activates it. No executable code ever leaves Cloudflare. The bundled execution engine — already in every build — interprets the config.

The hard invariant: downloaded plugins are data only. No eval. No WASM. No runtime scripting. No native loading. A plugin is a ProviderConfig + ProcessorConfig bundle that the engine already knows how to run.

In-App

Cloudflare R2

fetch + verify

download

pass

pass

activate

plugins/index.json

(signed catalog)

plugins///plugin.json

+ plugin.sig

Provider Registry

Service

Trust layer

(Ed25519 verify)

Schema validator

Execution engine

(preload.js + Rust)

Provider store

(SQLite)

Plugin schema

A plugin package is a JSON document with two functional halves and a trust envelope:

  • provider_config — transport spec: how to open the bank’s page, navigate auth, and fetch data (scraper half)
  • processor_config — parse spec: how to map the fetched bytes into Vec<TransactionRow> (mapper half)
  • integrity / signature — hash + Ed25519 detached signature over the canonical payload
  • constraints — engine version range and platform compatibility
  • redaction_hints — declared map of sensitive fields so masking stays enforced for downloaded specs

No field in the schema may contain executable snippets. Any free-form script surface is rejected at schema-validation time.

The scraper engine

The scraper is the interesting half. It runs entirely inside a bundled preload.js that reads window.providerConfig and adapts its behaviour from config fields alone. Adding a new bank pattern requires zero new preload.js code — only a new config bundle.

The fetch_url field selects the extraction strategy:

StrategyHow data is obtained
https://...HTTP fetch within the authenticated WebView session (AMEX pattern)
extract://nextjs-rscExtracts from the RSC flight payload (self.__next_f) — Next.js App Router banks (SEB/Spendwise)
extract://nextjs-dataExtracts from the __NEXT_DATA__ script tag — Next.js Pages Router banks
extract://script-jsonScans inline <script> tags for embedded JSON matching a declared dataArrayKey

Navigation and auth are also declarative: readySelector waits for a DOM element before triggering the fetch, autoClickSelectors clicks elements in sequence (login buttons, BankID prompts, cookie banners), and FetchValue::Prefetch extracts a CSRF token or session ID from one endpoint before the main fetch fires.

Trust and distribution

The app pins one or more Ed25519 trust-root public keys in the bundle. Every plugin payload is hashed, the hash is signed, and the app verifies the signature against the pinned root before the plugin can progress past the download step. A revocation list in the signed catalog blocks specific versions from activating.

Update lifecycle:

  1. App fetches signed index.json catalog on schedule or on manual refresh
  2. For each enabled plugin, compare installed version against catalog target
  3. Download candidate, verify hash + signature + schema
  4. Stage as pending; mandatory dry-run gate against the execution engine
  5. Promote to active only after a clean dry run
  6. If the catalog is unreachable, the last verified version keeps running

The mapper layer

Once the scraper delivers raw bytes (Excel, JSON, CSV) to Rust, ProcessorConfig takes over. It is a bounded typed vocabulary: format selectors (Json, Excel, Csv), field mappings with optional transforms (ParseDate, ToNumber, Regex), and filters (Equals, Contains, IsEmpty). No code; entirely data. The mapper side is well-covered and fast to test — it runs in the standard Rust test harness with synthetic fixture inputs, no WebView required.

Worked example

  1. A new SEB provider plugin is published: plugins/seb.se/2026.05.17.1/plugin.json + .sig appear in the Cloudflare catalog.
  2. The app fetches the updated index.json and notices seb.se has a new version.
  3. It downloads plugin.json and plugin.sig, computes the hash of the canonical payload, and verifies the Ed25519 signature against the pinned trust root.
  4. It runs the schema validator — confirms provider_config, processor_config, redaction_hints, and constraints are present and well-formed; no executable fields exist.
  5. The plugin is staged as pending. The execution engine does a dry run against the SEB fixture (if available), confirms it produces Vec<TransactionRow> matching the expected baseline.
  6. The plugin is promoted to active in the provider store.
  7. On the next sync, the scraper opens a WebView to the SEB login page, drives navigation via autoClickSelectors (BankID button), waits for the RSC payload via readySelector, extracts self.__next_f data, and hands it to Rust. The ProcessorConfig maps the RSC fields into TransactionRow entries.

Recap

  • Provider plugins are signed data bundles — declarative ProviderConfig + ProcessorConfig pairs — interpreted by a bundled execution engine; no downloaded code ever runs.
  • Every plugin passes Ed25519 signature verification, JSON schema validation, and a mandatory dry-run gate before it can activate.
  • The scraper engine uses config fields (readySelector, autoClickSelectors, fetch_url strategy, FetchValue::Prefetch) to navigate any bank’s login flow and extract transactions without per-bank code.

What changed {#what-changed}

2026-05-18 — CAS-3499 Declarative Provider Plugins (shipped)

The declarative plugin system shipped across Phases A–E. Provider plugins are now downloadable from Cloudflare R2, signature-verified (Ed25519), schema-validated, and activated inside the app without any executable code leaving the trust boundary. The full catalog UI is live in Settings. This architecture doc was updated from proposal to delivered status.

See: CHANGELOG → 2026-05-18