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.
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 intoVec<TransactionRow>(mapper half)integrity/signature— hash + Ed25519 detached signature over the canonical payloadconstraints— engine version range and platform compatibilityredaction_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:
| Strategy | How data is obtained |
|---|---|
https://... | HTTP fetch within the authenticated WebView session (AMEX pattern) |
extract://nextjs-rsc | Extracts from the RSC flight payload (self.__next_f) — Next.js App Router banks (SEB/Spendwise) |
extract://nextjs-data | Extracts from the __NEXT_DATA__ script tag — Next.js Pages Router banks |
extract://script-json | Scans 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:
- App fetches signed
index.jsoncatalog on schedule or on manual refresh - For each enabled plugin, compare installed version against catalog target
- Download candidate, verify hash + signature + schema
- Stage as
pending; mandatory dry-run gate against the execution engine - Promote to
activeonly after a clean dry run - 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
- A new SEB provider plugin is published:
plugins/seb.se/2026.05.17.1/plugin.json+.sigappear in the Cloudflare catalog. - The app fetches the updated
index.jsonand noticesseb.sehas a new version. - It downloads
plugin.jsonandplugin.sig, computes the hash of the canonical payload, and verifies the Ed25519 signature against the pinned trust root. - It runs the schema validator — confirms
provider_config,processor_config,redaction_hints, andconstraintsare present and well-formed; no executable fields exist. - The plugin is staged as
pending. The execution engine does a dry run against the SEB fixture (if available), confirms it producesVec<TransactionRow>matching the expected baseline. - The plugin is promoted to
activein the provider store. - 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 viareadySelector, extractsself.__next_fdata, and hands it to Rust. TheProcessorConfigmaps the RSC fields intoTransactionRowentries.
Recap
- Provider plugins are signed data bundles — declarative
ProviderConfig + ProcessorConfigpairs — 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_urlstrategy,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.