# Architecture

> Why the runtime is a hardened WebView rather than a two-thread engine, and how synthetic origins produce isolation.

Source: https://miniapp.sollar.com/platform/architecture/

---

A mini app runs in a **hardened WebView, one per app, at its own synthetic origin**. That sentence
contains the two decisions that shape everything else.

## Why not a two-thread runtime

WeChat's runtime splits a mini program into a render layer in a WebView and a logic layer in a
separate JavaScript engine with no DOM, relayed by the native client. It is a well-understood design
and Sollar does not use it, on three grounds.

### 1. Apple requires WebKit, worldwide

Guideline 2.5.6: apps that browse the web *"must use the appropriate WebKit framework and WebKit
JavaScript."* The Embedded Browser Engine Entitlement that would permit an alternative exists **only
in the European Union**, and qualifying requires 90% on the Web Platform Tests, 80% on Test262, a
memory-safe implementation language, and a 30-day CVE remediation commitment. That is out of reach,
and it would not cover the rest of the world in any case.

### 2. Performance argues the same way, which is counter-intuitive

WKWebView's WebContent process **has JIT**. A JavaScript engine embedded in the app's own process
**does not** — Apple does not grant the entitlement for writable-executable memory to ordinary apps.

So a custom two-thread runtime with its own JavaScriptCore instance would be *slower* on iOS than
the WebView it was meant to improve on. This is the same ceiling WeChat's own logic layer runs into,
and it is not a small effect.

### 3. Lark already tried it and reversed

Lark's two-thread "gadget" runtime is deprecated in favour of Web Apps over WebView. When the
company with the closest requirements and full control of both clients abandons the approach, that
is evidence rather than opinion.

### What about Android?

Android reaches the same architecture by a different and thinner legal path. Google Play's *Device
and Network Abuse* policy exempts from its downloaded-code prohibition anything that runs *"in a
virtual machine or an interpreter where either provides indirect access to Android APIs (such as
JavaScript in a webview or browser)."*

Note the asymmetry, because it is the reverse of what most people assume: **Apple has a dedicated
policy that names mini apps and permits them** (Guideline 4.7). **Google does not.** Android depends
on a generic interpreter exemption. That constrains the design — every execution path on Android
must end in interpreted JavaScript with indirect, mediated access through the Sollar bridge, never a
native code loader.

## Isolation comes from the origin

Each mini app is served from a synthetic origin of its own:

| Platform | Mechanism | Origin |
|---|---|---|
| Android | `WebViewAssetLoader` + `shouldInterceptRequest` | `https://<appid>.miniapp.localhost/` |
| iOS | `WKURLSchemeHandler` | A per-app scheme, treated as a distinct origin |

From that one decision, the browser engine gives:

- **Storage partitioning** — `localStorage`, IndexedDB, the Cache API and cookies are separated by
  origin. Not by a directory convention the runtime has to remember; by the same-origin policy.
- **CSP scope** — a policy applies to one mini app and cannot leak to another.
- **A bridge boundary** — `allowedOriginRules` on Android binds the message channel to exactly one
  origin.

This is the difference between a rule the runtime remembers to enforce and a rule it cannot violate.
Cache reuse between mini apps is the first of the six vulnerability categories in
[the security model](/platform/security-model/), and it exists in other platforms precisely because
they chose shared directories with runtime-enforced paths.

> **CAUTION**
On iOS, whether a custom scheme served through `WKURLSchemeHandler` counts as a **secure context**
is verified per release and tracked as an open risk. If it fails, `crypto.subtle` and service workers
become unavailable inside mini apps and the encrypted-storage design changes. This is stated here
rather than assumed away.

`WKWebsiteDataStore(forIdentifier:)` gives an additional per-mini-app store on iOS, and
`nonPersistent()` gives an ephemeral mode. On Android, `setDataDirectorySuffix` looks like the
equivalent but is not usable for this: it is process-wide and settable once, so it cannot separate
apps within a running client. Origin separation solves what it cannot.

## The bridge

| Platform | Mechanism | Never |
|---|---|---|
| Android | `WebViewCompat.addWebMessageListener` with `allowedOriginRules` | `addJavascriptInterface` |
| iOS | `WKScriptMessageHandlerWithReply` in a dedicated `WKContentWorld` | — |

`addJavascriptInterface` has no origin control and is the vector of CVE-2012-6636, where
attacker-controlled JavaScript reached arbitrary Java methods by reflection. The usual mitigation —
"only affects `targetSdk` ≤ 16" — is incomplete, and AOSP's own documentation says so: apps
targeting later versions remain vulnerable when running on Android before 4.2. Sollar's `minSdk`
makes that historical, but the operational lesson stands: one API has origin control and the other
does not.

`allowedOriginRules` matches scheme, host and port. **The path is ignored.** A design that assumes
the bridge can be restricted to a subdirectory is a design built on a misreading.

> **NOTE**
**`WKContentWorld` is not a security boundary.** It isolates JavaScript variables; it does **not**
isolate the DOM, and DOM mutations are visible across every world. Apple documents it as a solution
to script and namespace conflicts, not as a capability restriction.

It is the right tool for stopping a mini app from overwriting the bridge function. It is the wrong
tool for hiding data from one.

## Where permission decisions happen

**In native code, against the signed manifest. Never in JavaScript.**

```
mini app JS  →  [sollar.* shim]  →  native bridge  →  ┌────────────────────────┐
                                                      │ 1. manifest declares   │
                                                      │    this scope?         │
                                                      │ 2. user granted?       │
                                                      │ 3. tenant allows?      │
                                                      │ 4. within quota?       │
                                                      └───────────┬────────────┘
                                                                  ▼
                                                             capability
```

The JavaScript shim is ergonomics. If the only thing between a call and a capability were a check in
JavaScript, there would be no check — a mini app controls its own JS environment by definition.

## Lifecycle

```
   install ──▶ launch ──▶ foreground ⇄ background ──▶ suspended ──▶ destroyed
                            │                                          │
                            └──── agent invocation (headless) ─────────┘
```

- **Launch** — package verified against its Merkle root, manifest parsed, origin established, bridge
  installed, entry page loaded.
- **Background** — timers throttled, network paused. The instance is retained for a short window and
  then suspended.
- **Suspended** — the renderer may be discarded to reclaim memory; `setRendererPriorityPolicy` on
  Android makes the mini app's renderer sacrificable before the host process. Returning re-launches
  and restores route state.
- **Headless** — an agent invocation can start or resume an instance without showing it. `ctx` tells
  your handler which case it is in.

Nothing survives destruction except origin storage and `sollar.storage`. Treat in-memory state as
disposable.

## Network

Every request passes a **native interceptor** that checks the host against `manifest.network.connect`
before it leaves the device.

CSP is a second layer, not the primary one — a page can rewrite its own CSP at runtime, but it
cannot reach the native interceptor. This ordering is what stops the third vulnerability category,
silent exfiltration of sensitive data to an undeclared host.

Changing the destination list requires a new version, a new signature, and a new gate. That is the
point of it.

## Quotas

A mini app cannot degrade Sollar. Per instance: a ceiling on concurrent connections, on storage, and
on memory — with the instance destroyed rather than the host running out — plus a rate limit per
bridge method.

## What was considered and rejected

| Option | Why not |
|---|---|
| A two-thread runtime with an embedded JS engine | 2.5.6, plus the JIT asymmetry above |
| FinClip and similar container SDKs | A third-party runtime inside the client's trust boundary; opaque to review |
| Quick App | Vendor alliance runtime, Android-only, no iOS story |
| kbone / Taro compile-to-mini-program | Solves cross-compiling to WXML, a problem Sollar does not have |
| React Native or Flutter per mini app | Native code from third parties, downloaded — the exact thing both stores prohibit |

Taro deserves a note because Sollar's own earlier stack study chose it. It compiles React or Vue into
WeChat's WXML/WXSS dialect. If the runtime is the web platform, there is no dialect and no problem to
solve. The Chinese cross-compile ecosystem is also contracting: Remax archived 2024-03-07, WePY
2026-03-18, Chameleon 2026-04-15. Taro returns to the table only if Sollar ever wants to *import*
WeChat mini programs, which is a different goal and should be decided as one.
