Skip to content

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.

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.

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

Section titled “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.

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.

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.

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

PlatformMechanismOrigin
AndroidWebViewAssetLoader + shouldInterceptRequesthttps://<appid>.miniapp.localhost/
iOSWKURLSchemeHandlerA per-app scheme, treated as a distinct origin

From that one decision, the browser engine gives:

  • Storage partitioninglocalStorage, 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 boundaryallowedOriginRules 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, and it exists in other platforms precisely because they chose shared directories with runtime-enforced paths.

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.

PlatformMechanismNever
AndroidWebViewCompat.addWebMessageListener with allowedOriginRulesaddJavascriptInterface
iOSWKScriptMessageHandlerWithReply 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.

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.

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.

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.

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.

OptionWhy not
A two-thread runtime with an embedded JS engine2.5.6, plus the JIT asymmetry above
FinClip and similar container SDKsA third-party runtime inside the client’s trust boundary; opaque to review
Quick AppVendor alliance runtime, Android-only, no iOS story
kbone / Taro compile-to-mini-programSolves cross-compiling to WXML, a problem Sollar does not have
React Native or Flutter per mini appNative 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.