> For the complete documentation index, see [llms.txt](https://basedapp.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://basedapp.gitbook.io/docs/integrations/mini-apps-platform/05-js-runtime.md).

# JS runtime

How the JS engine works in practice: which package owns what, the dual-isolate model, the wire format on the SendPort, the boot/event/rerender flows, the CPU watchdog, and where the implementation diverges from spec aspirations.

If you are a partner shipping mini-app code, the parts you need are **Engine choice**, **Disallowed JS features**, and **Performance budget**. The rest is for engineers maintaining the runtime.

## Engine choice

**Decision: QuickJS via** [**`flutter_js`**](https://pub.dev/packages/flutter_js)**.**

| Engine                           | Pros                                                                                                | Cons                                                                                                                                                              | Decision    |
| -------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| **QuickJS** (via `flutter_js`)   | Tiny (\~210KB), spec-compliant ES2020, sync evaluation, mature Flutter binding, App Store-friendly. | No JIT (pure interpreter) — slower for heavy compute. No `JS_SetInterruptHandler` exposed by the Flutter binding, so CPU bounding has to happen at the Dart edge. | ✅ v1        |
| **JavaScriptCore** (iOS bundled) | Fast, JIT on iOS.                                                                                   | Android bundling cost, license/perf inconsistency.                                                                                                                | ❌           |
| **Hermes** (Meta's RN engine)    | Fast cold start, AOT bytecode.                                                                      | No clean Flutter binding; couples us to RN's release cadence.                                                                                                     | ❌           |
| **V8**                           | Fastest, JIT.                                                                                       | Massive binary cost; JIT is disallowed by Apple guideline 2.5.2 outside `WKWebView`.                                                                              | ❌           |
| **Wasm** (`wasm_run`)            | Type-safe, language-agnostic, sandboxed by spec.                                                    | Author DX rough today; toolchains not partner-friendly.                                                                                                           | Revisit v2. |

QuickJS has zero JIT, runs as a pure interpreter, and ships compiled into the host binary. This satisfies App Store guideline 4.7 (interpreted code is allowed) and removes the runtime-codegen risk entirely.

`getJavascriptRuntime()` dispatches by platform — `QuickJsRuntime2` (FFI) on Android/Windows/Linux, `JavascriptCoreRuntime` (FFI to system JSC) on iOS/macOS. Both are FFI-only — no platform channels — which is what lets the runtime live in a non-root Dart isolate without `BackgroundIsolateBinaryMessenger.ensureInitialized()`.

The interpreter is fast enough for UI logic, async network, and signing requests. It is **not** fast enough for:

* Heavy crypto (use `based.signing.*` host calls instead).
* Real-time charting compute (host renders charts; mini-apps pass data).
* Image decoding (host renders; mini-apps pass URLs).

These constraints are encoded in the widget catalog and host contract — partners don't need to worry about engine internals.

## Implementation layout

The runtime lives in `flutter/packages/based_js_runtime/`. Files relevant to this doc:

| File                                    | Owns                                                                                                                                                                       |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lib/src/runtime/mini_app_runtime.dart` | Public `MiniAppRuntime` class. Host-side proxy: spawns the worker, serializes events/rerenders, owns `vdom`/`state` notifiers, drives the bridge dispatcher.               |
| `lib/src/runtime/js_worker.dart`        | Worker-isolate top-level entrypoint. Owns the QuickJS engine, the three `sendMessage` channels (`based-bridge`, `based-console`, `based-rerender`), and microtask pumping. |
| `lib/src/runtime/js_shim.dart`          | The JS prelude (`basedRuntimePrelude`) injected before partner code. Defines `based.*`, `ui.*`, `state.*`, `__based_runtime__`.                                            |
| `lib/src/runtime/console_buffer.dart`   | Bounded ring buffer for `console.*` output.                                                                                                                                |
| `lib/src/runtime/runtime_state.dart`    | The `MiniAppRuntimeState` enum.                                                                                                                                            |
| `lib/src/bridge/bridge_message.dart`    | `BridgeCall` / `BridgeReply` / `BridgeEvent` wire types (the JSON over `based-bridge`).                                                                                    |
| `lib/src/bridge/dispatcher.dart`        | `BridgeDispatcher` — routes `(ns, method)` to handlers, runs permission + rate-limit checks.                                                                               |
| `lib/src/bridge/host_handlers.dart`     | Hand-written wiring from `BasedMiniAppHost` to the dispatcher. (Will be code-generated; see `06-bridge-and-codegen.md`.)                                                   |

All host services, the dispatcher, and the permission enforcer live on the **host (UI) isolate**. Only the JS engine itself lives in the worker.

## Two-isolate model

```mermaid
graph LR
    subgraph host["Host (UI) isolate"]
        MR["MiniAppRuntime"]
        VDOM["vdom: ValueNotifier&lt;UiNode?&gt;"]
        STATE["state: ValueNotifier&lt;State&gt;"]
        DISP["BridgeDispatcher<br/>(PermissionEnforcer + RateLimiter)"]
        SVC["host services<br/>(signing, http, etc.)"]
        PORTS["Isolate handle + SendPort/ReceivePort"]
        MR --> VDOM
        MR --> STATE
        MR --> DISP
        MR --> SVC
        MR --> PORTS
    end

    subgraph worker["Worker isolate"]
        ENTRY["miniAppJsWorkerEntrypoint"]
        QJS["QuickJS via flutter_js"]
        CHANS["onMessage channels:<br/>based-bridge / -console / -rerender"]
        COAL["rerender coalescer<br/>(1 in flight + dirty flag)"]
        SER["command serializer (Future tail)"]
        ENTRY --> QJS
        ENTRY --> CHANS
        ENTRY --> COAL
        ENTRY --> SER
    end

    PORTS <==>|SendPort / ReceivePort| ENTRY
```

Why a Dart isolate, not just a separate JS context?

1. **CPU isolation.** A JS infinite loop blocks the worker, never the host UI thread.
2. **Hard kill.** A runaway worker can be killed via `Isolate.kill(priority: Isolate.immediate)`. We use this on CPU-watchdog timeouts.
3. **Memory boundary.** A runaway QuickJS heap can't drag down host memory; the isolate is freed on dispose/kill.
4. **Forced async.** Even a synchronous-looking JS handle has to round-trip through `SendPort`, which serializes naturally.
5. **Future parallelism.** The boundary already exists when v2 wants backgrounded mini-apps streaming alongside an interactive one.

Cost: spawning an isolate is \~10–30ms on modern devices. We don't pre-warm today; first launch pays the spawn cost.

## Wire format (host ↔ worker)

Plain Maps over `SendPort`/`ReceivePort`. Messages are tagged by `t` (type). All values are `dart:core` types so they pass between isolates without `Isolate.exit` transfer hacks.

### Host → worker (commands)

| `t`          | Fields                       | Purpose                                                                                                                                  |
| ------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `boot`       | `id`, `source`               | Evaluate the bundle source (prelude + partner code), pump microtasks, then call `__based_runtime__.callApp()` and return the first tree. |
| `fire-event` | `id`, `eventRef`, `argsJson` | Invoke the JS-side closure registered for an `evt:N` ref. Returns the post-handler tree.                                                 |
| `rerender`   | `id`                         | Re-evaluate `__based_runtime__.callApp()` and return the new tree. (Public `MiniAppRuntime.rerender()`.)                                 |
| `reply`      | `callId`, `escaped`          | Resolve a previously-issued bridge call. `escaped` is the JSON reply, double-encoded as a JS string literal.                             |
| `shutdown`   | —                            | Dispose the JS context and call `Isolate.exit()`.                                                                                        |

### Worker → host (events / replies)

| `t`           | Fields                    | Purpose                                                                                                      |
| ------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `ready`       | `port`                    | Handshake. Worker's command port. Sent once on spawn.                                                        |
| `eval-done`   | `id`, `isError`, `result` | Correlated reply for `boot` / `fire-event` / `rerender`.                                                     |
| `tree-push`   | `isError`, `result`       | **Autonomous** rerender result: produced when JS-side `state.set` fires `based-rerender`. No correlation id. |
| `bridge-call` | `raw`                     | Raw JSON of a `BridgeCall` (JS asked the host for something).                                                |
| `console`     | `level`, `message`        | `console.{log,warn,error,info,debug}` output.                                                                |
| `fatal`       | `message`                 | The worker's command handler caught an exception.                                                            |

Two important properties of this format:

1. **JS-side bridge calls are forwarded raw**, not re-typed at the worker. The worker doesn't know about `BridgeCall`; it just hands the JSON to the host.
2. **Bridge replies are double-encoded.** The host JSON-encodes the reply, then JSON-encodes that string, so it can be embedded as `__based_runtime__.deliverReply(id, "<escaped>")` without escaping pain.

Inside the worker, command handling is serialized via a Future tail (`tail = tail.then(...)`) so two commands can't interleave their `evaluateAsync` and `pumpMicrotasks` calls. On the host, `_fireEvent` is also serialized via `_fireEventTail` so vdom updates land in dispatch order even when taps arrive faster than the worker round-trips.

## Boot flow

```mermaid
sequenceDiagram
    participant H as Host
    participant W as Worker
    H->>W: Isolate.spawn(entrypoint, sendPort)
    Note over W: ReceivePort +<br/>getJavascriptRuntime(xhr:false)<br/>register based-bridge / -console / -rerender
    W-->>H: {t:'ready', port}
    H->>W: {t:'boot', id, source}
    Note over W: js.evaluate(source)<br/>(prelude + bundle, sync)
    Note over W: pumpMicrotasks()<br/>(drain top-level Promise resolutions)
    Note over W: await js.evaluateAsync('__based_runtime__.callApp();')
    Note over W: pumpMicrotasks()
    W-->>H: {t:'eval-done', id, isError, result}
    Note over H: decode UiNode → vdom.value<br/>state.value = mounted
```

`xhr: false` is passed to `getJavascriptRuntime()` so the JS context has no `fetch`/`XMLHttpRequest`. All network goes through `host.http.*` over the bridge. Disabling `xhr` keeps the worker FFI-only.

The whole boot (parse + first `App()`) is bounded by `_bootTimeout` (5s default). Previously only the `App()` call was bounded — a stuck parse used to hang the UI forever; with the worker we can let the host time out and kill.

## Bridge round-trip

A typical `await based.http.get(url)` inside a mini-app:

```mermaid
sequenceDiagram
    participant JS as JS (worker)
    participant WC as Worker code
    participant H as Host (UI)
    JS->>WC: based.http.get(url)<br/>via __basedCall<br/>(returns Promise)
    Note over JS: sendMessage('based-bridge',<br/>BridgeCall JSON)
    WC->>H: post {t:'bridge-call'}
    Note over H: BridgeMessage.fromJson
    Note over H: _dispatcher.dispatch:<br/>• enforcer.check (permissions)<br/>• rateLimiter.record<br/>• host.http.get(url)
    Note over H: BridgeReply built
    H-->>WC: {t:'reply', callId, escaped}
    WC-->>JS: deliverReply(id, json)
    Note over JS: resolves Promise<br/>(microtasks pumped)
```

Permission and rate-limit checks happen **before** the host method runs. An unlisted domain or a method the manifest didn't declare fails with `MiniAppHostException` before any host code touches user data. Rate-limit buckets are namespace-scoped (`signing`/`tx` 30/min, `nav.openDeepLink` 10/min, `analytics` 200/min, `global` 1000/min).

Note that `flutter_js`'s `sendMessage` callback is **synchronous** — it can't `await`. The JS shim (`js_shim.dart`) records the call as a pending Promise keyed by id, fires `sendMessage` as fire-and-forget, and waits for `__based_runtime__.deliverReply(id, replyJson)` to resolve it. That call is what the `reply` command makes the worker do; `pumpMicrotasks()` runs straight after to advance the resolved Promise's `.then` continuations.

## Rerender flow

There are two paths to `vdom`:

### Autonomous (the common case)

A JS event handler calls `state.set(k, v)`. The shim coalesces multiple `state.set` calls in the same tick into one microtask, which fires `sendMessage('based-rerender', null)`. The worker's `based-rerender` listener:

1. If a rerender is already in flight, set a `dirty` flag and return — the in-flight one will repeat.
2. Otherwise, mark in-flight and run a loop: `evaluateAsync('__based_runtime__.callApp();')` → pump microtasks → post `tree-push` → if `dirty` was set, repeat.

This **coalescing** is the worker-side defence against burst traffic. Without it, a rapid `state.set` loop produced one `tree-push` per call and piled them up in the host's SendPort queue. With it, at most one in-flight evaluation per cycle.

### Explicit (`fire-event`, `rerender`, `boot`)

The host issues a correlated command. The worker evaluates and posts `eval-done` with the same `id`. The host reads `eval-done.result` directly into `vdom.value`. No coalescing needed — these are user-driven, one-at-a-time.

In sync event handlers, the JS shim's `__fireEvent` already returns the post-handler tree, so the `eval-done` for `fire-event` carries the new tree. The autonomous `based-rerender` from `state.set` may also fire shortly after, producing one redundant `tree-push`. This is a known double-evaluation; the trees are identical, so the second `vdom.value =` is a no-op for Flutter (same structural diff).

## Event flow

```mermaid
sequenceDiagram
    participant U as User
    participant R as UiNodeRenderer
    participant MR as MiniAppRuntime
    participant W as Worker
    U->>R: tap (Button)
    R->>MR: UiRenderContext.onEvent(ref, args)
    Note over MR: _fireEventTail = _fireEventTail.then(_doFireEvent)<br/>(host-side serialization)
    MR->>W: {t:'fire-event', id, eventRef, argsJson}
    Note over W: js.evaluateAsync<br/>__based_runtime__.fireEvent("evt:N", {...})<br/>pump microtasks
    W-->>MR: {t:'eval-done', id, result}
    Note over MR: _pending[id] completes<br/>decode tree → vdom.value = newTree
```

JS-side closures cannot serialize across the bridge. The shim stores them in a numeric table; only the integer id crosses, encoded as `{ "$ref": "evt:<id>" }` in `UiNode` JSON. The renderer carries refs as opaque strings; `UiRenderContext.onEvent` is what dereferences them back through `__based_runtime__.fireEvent`.

## CPU watchdog (Dart-side, post-spec)

{% hint style="info" %}
**Spec aspiration:** install `JS_SetInterruptHandler` and have QuickJS abort runaway code with `InterruptedError`.
{% endhint %}

{% hint style="warning" %}
**Reality:** `flutter_js` does not expose `JS_SetInterruptHandler`. We bound CPU at the Dart edge.
{% endhint %}

`MiniAppRuntime._sendEval` wraps the `Completer` for each `eval-done` in a `.timeout(budget)`:

| Op           | Default budget |
| ------------ | -------------- |
| `boot`       | 5 s            |
| `fire-event` | 200 ms         |
| `rerender`   | 200 ms         |

On `TimeoutException`:

1. Remove the pending Completer (the late `eval-done` will be ignored).
2. Append a `CPU budget exceeded during "<op>"` line to the console buffer.
3. **Hard-kill the worker** via `Isolate.kill(priority: Isolate.immediate)`.
4. Mark `_terminated = true`, fail any other pending Completers (so awaiters unblock), set `_state = errored`.
5. Throw `MiniAppHostException(code: timeout)` so the calling path can render the error.

After termination the host stays alive — `vdom`/`state` notifiers are still alive and the screen sees `state == errored`. The user has to back out and re-enter to relaunch. There is no auto-recovery; a hung mini-app should not silently restart.

## Worker error and exit handling

`Isolate.spawn` is called with `errorsAreFatal: true`, plus `onError` and `onExit` ports. Two listeners on the host:

* **`errorPort`** — fires on uncaught Dart errors inside the worker (FFI crash, bug in the worker handler). The host calls `_terminate('worker error')`.
* **`exitPort`** — fires on isolate exit. The graceful shutdown path (`dispose() → 'shutdown' command → Isolate.exit()`) also fires this; `_terminate` is idempotent against `_disposed`, so the cleanup short-circuits. Unexpected exits (OOM, killed by OS) trigger termination the same way.

`_terminate` is the single chokepoint: kill the isolate (no-op if already dead), fail all pending Completers, set state errored, log the reason, close listener ports. Both `dispose()` and `_terminate()` are idempotent.

## Lifecycle states

Defined in `runtime_state.dart`:

| State      | Reachable today | Notes                                                                                                              |
| ---------- | --------------- | ------------------------------------------------------------------------------------------------------------------ |
| `idle`     | ✓               | Constructor returns here.                                                                                          |
| `booting`  | ✓               | Set at the start of `boot()`, before `Isolate.spawn`.                                                              |
| `loading`  | ✓               | Worker is up; bundle being evaluated.                                                                              |
| `mounted`  | ✓               | First `UiNode` in `vdom`. Steady state for foreground use.                                                         |
| `active`   | ✗               | **Defined but unreachable in v1.** Reserved for the foreground/active distinction once host-driven pause is wired. |
| `paused`   | ✗               | **Defined but unreachable in v1.** Reserved for backgrounded mini-apps.                                            |
| `errored`  | ✓               | Set on boot failure, fire-event JS error, CPU timeout, worker error/exit.                                          |
| `disposed` | ✓               | Set in `dispose()`. The notifiers are torn down after this; further setters are guarded.                           |

`active` and `paused` will become live once we add `onPause`/`onResume` lifecycle hooks (see Mini-app side hooks below). For now, `mounted` is the steady state.

### Mini-app side hooks

The mini-app default export is an object:

```js
export default {
  // Required — first render.
  App({ ui, state, host }) { ... },

  // Optional — called when the mini-app is mounted.
  onMount({ host }) { ... },          // not yet wired
  onPause({ host }) { ... },          // not yet wired
  onResume({ host }) { ... },         // not yet wired
  onDispose({ host }) { ... },        // not yet wired
};
```

Only `App` is invoked today. The other hooks are reserved.

## Resource limits

Spec defaults; configurable per mini-app via tier:

| Resource                   | Standard                                       | Verified                | First-party             | Enforced today?                                                                      |
| -------------------------- | ---------------------------------------------- | ----------------------- | ----------------------- | ------------------------------------------------------------------------------------ |
| JS heap                    | 32 MB                                          | 96 MB                   | 96 MB                   | ✗ (would need QuickJS heap-limit FFI binding)                                        |
| JS stack depth             | 1024 frames                                    | 1024                    | 1024                    | partial — `flutter_js` honors a stack-size constructor arg, not exposed at our layer |
| CPU per op                 | 200 ms                                         | 200 ms                  | 200 ms                  | ✓ (Dart `.timeout()` + `Isolate.kill`, see above)                                    |
| Active timers              | 64                                             | 256                     | 256                     | ✗                                                                                    |
| HTTP concurrency           | 8                                              | 16                      | 16                      | ✗ — but bridge-call rate is limited (`global` 1000/min)                              |
| Total HTTP bytes / session | 50 MB                                          | 200 MB                  | 200 MB                  | ✗                                                                                    |
| Storage quota              | per manifest (≤ 50 MB)                         | per manifest (≤ 200 MB) | per manifest (≤ 200 MB) | ✗                                                                                    |
| Bundle size                | 4 MB / 16 MB                                   | 8 MB / 32 MB            | 8 MB / 32 MB            | ✗ — should be enforced at install                                                    |
| Bridge call rate           | namespace token bucket (e.g. `signing` 30/min) | same                    | same                    | ✓ (`RateLimiter`)                                                                    |

Tier is platform-side, not in the manifest. The CPU watchdog and bridge rate limiter are the two enforced gates today; the rest are planned.

## Error handling

Three sources of error:

1. **JS exceptions inside `App`/event handlers** — caught by the JS shim's `__reportError`/`__errorTree` helpers and rendered as a contained error subtree (`__errorTree`) instead of crashing the runtime. The host console buffer gets a structured trace.
2. **Bridge errors** — type mismatches, permission violations, schema failures. Surfaced pre-emptively: the call never reaches the host method. The dispatcher returns a `BridgeReply.err` with a stable `code` that the JS shim turns into a thrown `Error` with `e.code`/`e.message`/`e.details`.
3. **Host errors** — exceptions in Dart `BasedMiniAppHost` impls. Wrapped in `MiniAppHostException` with a stable `code`, serialized over the bridge, surfaced to JS the same way.

Stable codes are documented in `13-changelog.md` and never repurposed; new codes are additive only.

Partner-facing error contract:

```js
try {
  await based.signing.signEvm(req, { reason: 'Stake' });
} catch (e) {
  // e.code: stable string, e.g. 'permission_denied', 'user_rejected', 'network'
  // e.message: human-readable, may be tenant-localized
  // e.details: optional structured payload
}
```

## Disallowed JS features

Blocked at bundle build time (CLI, future) and re-checked at install:

* `eval()`, `new Function(...)` — disallowed. Bundles parsed at install fail validation if these strings appear in non-string-literal positions.
* Dynamic `import()` — disallowed.
* Any access to `globalThis` keys not on the allowlist:
  * Provided: `based`, `console`, `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`.
  * Standard ECMAScript: `Promise`, `JSON`, `Math`, `Date`, `Map`, `Set`, `WeakMap`, `WeakSet`, `Symbol`, `Reflect`, `Proxy`, `Error` and subclasses, `Number`, `String`, `Boolean`, `Array`, `Object`, `RegExp`, `BigInt`, `ArrayBuffer`, `Uint8Array` family, `TextEncoder`, `TextDecoder`, `URL`, `URLSearchParams`.
  * `Intl.*` — `NumberFormat`, `DateTimeFormat`, `RelativeTimeFormat`, `PluralRules`, `Collator`. Where QuickJS lacks ICU data, the runtime falls back to a small host-mediated polyfill matching the host's locale.
* Native module access — none. There is no FFI surface from mini-app JS.

These restrictions exist because hot-loading new code (Apple guideline 4.7) is allowed only for *interpreted* code that doesn't dynamically generate further code at runtime. Banning `eval` keeps us safely on the right side.

## Performance budget (informational)

| Operation                             | Target              | Notes                                                                        |
| ------------------------------------- | ------------------- | ---------------------------------------------------------------------------- |
| Cold isolate spawn                    | < 30 ms             | Today: pays full spawn on first launch (no pre-warm).                        |
| Boot (cached bundle, warm process)    | < 150 ms            | After first launch; QuickJS context creation + parse + first `App()`.        |
| Boot (fresh 1MB bundle)               | < 400 ms            | Parse-dominated.                                                             |
| `based.*` round trip (Dart sync impl) | < 2 ms              | Two SendPort hops + JSON round-trip + dispatcher.                            |
| `based.http.get` round trip           | network bound       |                                                                              |
| State change → re-render              | < 16 ms (one frame) | Ceiling: full-tree re-serialization. Large trees (>1000 nodes) regress this. |

The isolate move (`feat(mini-apps/A1)`) shifts the JS work off the UI thread but does **not** change the per-render serialization cost. The next-tier optimizations are tree-diff on the bridge and a `ListView.builder` wrapper in the renderer; both deferred until devtools traces show them as the bottleneck.

## Partner hot-reload (development mode)

In dev mode partners point the host at a local dev server (CLI command in `10-developer-experience.md`). The host:

1. Subscribes to a websocket pushed by the dev server.
2. On bundle change, the server pushes a notification.
3. Host disposes the current runtime and re-launches with the fresh bundle.
4. State is preserved opt-in by hashing storage and restoring it.

Dev mode requires a debug-build host; not available in production.

## Source maps

Production bundles are minified and ship `main.js.map` alongside.

* The host downloads but does not parse source maps for normal execution.
* On uncaught error, the runtime resolves stack frames against the source map before sending to telemetry. Partners get readable stacks in their dashboards.
* Source maps are not exposed to the running mini-app (no `eval`-style introspection).

## Gaps vs. spec

Tracked here so they don't get lost:

* **Pre-warmed isolate pool.** Spec says "pre-warm one idle isolate at host start so the first mini-app launches near-instantly." Today we spawn on demand. Each first launch pays \~10–30 ms.
* **QuickJS interrupt callback.** Bypassed via Dart timeout + `Isolate.kill`. Real fix is an upstream `flutter_js` change to expose `JS_SetInterruptHandler`. Today's behavior is a strict superset: the worker dies if the budget is exceeded, the host recovers cleanly.
* **Heap limit.** Not enforced. Needs `flutter_js` to expose QuickJS memory-limit API or we wrap the FFI ourselves.
* **Timer caps / HTTP concurrency / storage quota / bundle size at install.** All planned, none enforced.
* **`active` / `paused` lifecycle states.** Defined in the enum but never assigned. Will go live with `onMount`/`onPause`/`onResume` hooks.
* **Source map parsing for telemetry.** Not implemented.
* **CLI bundle validation** (no `eval`, no dynamic imports). Not implemented; today bundles are trusted at load time.

## Cross-references

* Architecture: [`02-architecture.md`](/docs/integrations/mini-apps-platform/02-architecture.md)
* Host contract: [`03-host-contract.md`](/docs/integrations/mini-apps-platform/03-host-contract.md)
* Manifest: [`04-manifest.md`](/docs/integrations/mini-apps-platform/04-manifest.md)
* Bridge JSON shape: [`06-bridge-and-codegen.md`](/docs/integrations/mini-apps-platform/06-bridge-and-codegen.md)
* UI nodes / renderer: [`07-ui-nodes-and-renderer.md`](/docs/integrations/mini-apps-platform/07-ui-nodes-and-renderer.md)
* Sandbox: [`08-security-and-sandbox.md`](/docs/integrations/mini-apps-platform/08-security-and-sandbox.md)
