> 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/06-bridge-and-codegen.md).

# Bridge & codegen

The bridge is the layer that lets JS in the mini-app isolate call Dart methods on `BasedMiniAppHost`. It is a generated artifact, not hand-written, and it is the single point at which permission enforcement and message validation happen.

## Wire protocol

Every interaction on the bridge is a JSON message. Two message types: `call` and `event`.

### `call` — JS → Dart

```json
{
  "kind": "call",
  "id": 42,
  "ns": "signing",
  "method": "signEvm",
  "args": [
    { "to": "0xVault…", "data": "0x…", "value": "0x0" },
    { "reason": "Stake USDC" }
  ],
  "miniAppVersion": "1.4.2",
  "targetSdkVersion": "1.0"
}
```

| Field              | Purpose                                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `kind`             | Always `"call"`.                                                                                                                 |
| `id`               | Caller-assigned integer; reply messages echo it.                                                                                 |
| `ns`               | The sub-interface namespace: `auth`, `signing`, `tx`, `http`, `storage`, `nav`, `ui`, `analytics`, `theme`, `clipboard`, `host`. |
| `method`           | The method name within the namespace.                                                                                            |
| `args`             | JSON array of arguments. Schema is generated from the Dart method signature.                                                     |
| `miniAppVersion`   | The mini-app's own version (from manifest). For telemetry.                                                                       |
| `targetSdkVersion` | The SDK version the mini-app was authored against. The bridge applies compat shims based on this.                                |

### Reply — Dart → JS

```json
{ "kind": "reply", "id": 42, "ok": true,  "value": { "signed": "0x…" } }
```

or

```json
{ "kind": "reply", "id": 42, "ok": false, "error": { "code": "user_rejected", "message": "User rejected the signing request" } }
```

### `event` — Dart → JS

For host-pushed events (route changes, theme updates, mini-app pause/resume).

```json
{ "kind": "event", "topic": "theme.changed", "payload": { "isDark": true } }
```

Mini-apps subscribe via `based.on('theme.changed', cb)`. Topics are documented; arbitrary topics are not allowed.

## Message validation

Every incoming `call` is validated **before** dispatching to the host method:

1. **Schema validation** — `ns.method` exists; `args` matches the generated parameter schema (types, lengths, regexes). Mismatch → reject with `code: "schema"`.
2. **Permission check** — generated from the method's `@RequiresPermission` annotation. Manifest check, then per-action check if any. Reject with `code: "permission_denied"`.
3. **Rate limit** — per namespace and global. Reject with `code: "rate_limited"`.
4. **Compat shim** — if `targetSdkVersion < currentSdkVersion`, apply argument transformations for renamed params, defaults, etc.
5. **Dispatch** — call the Dart method, await, marshal the result.

```mermaid
flowchart TB
    IN["Incoming call (JSON)"]
    S["1. Schema validation"]
    P["2. Permission check"]
    R["3. Rate limit"]
    C["4. Compat shim"]
    D["5. Dispatch to host method"]
    REJ["Reject with stable code<br/>(schema / permission_denied /<br/>rate_limited)"]
    IN --> S
    S -- ok --> P
    P -- ok --> R
    R -- ok --> C
    C --> D
    S -- fail --> REJ
    P -- fail --> REJ
    R -- fail --> REJ
```

Validation is rejection-only: a malformed call cannot reach a host method. This is the security boundary.

## Codegen

The bridge is **generated from the `BasedMiniAppHost` Dart interface** by a custom `build_runner` source generator (`tools/bridge_codegen/`).

### What is generated

For each method on `BasedMiniAppHost` (and sub-interfaces):

* A **Dart dispatcher** entry: registers `(ns, method) → (args, miniAppCtx) => Future<dynamic>`.
* A **JSON schema fragment** for the args (used at validation time).
* A **TypeScript declaration** for the JS API surface, shipped to partners as `@based/sdk-types`.
* A **JS shim** for the `based.<ns>.<method>` call, including:
  * argument marshalling (Dart-friendly JSON shapes),
  * Promise-based return,
  * typed error throwing (`{ code, message, details }`),
  * JSDoc / TSDoc with the same docs the Dart interface has.

### Source of truth

The Dart interface in `based_mini_app_sdk` is the single source of truth. Codegen runs on every build and on CI; PRs that change the interface without regenerating fail CI.

### Generated TS example (illustrative)

For:

```dart
abstract class BasedSigning {
  @RequiresPermission('signing.evm')
  Future<SignedTx> signEvm(EvmTxRequest req, {required String reason});
}
```

The generator produces:

```ts
// @based/sdk-types — generated, do not edit
export interface EvmTxRequest {
  to: `0x${string}`;
  data: `0x${string}`;
  value?: `0x${string}`;
  // …
}
export interface SignedTx { signed: `0x${string}`; hash?: string }

export interface BasedSigning {
  /** Requires permission: signing.evm */
  signEvm(req: EvmTxRequest, opts: { reason: string }): Promise<SignedTx>;
  // …
}
```

…and the runtime JS shim:

```js
based.signing.signEvm = (req, opts) =>
  __based_call('signing', 'signEvm', [req, opts]);
```

Partners get full type-check + autocomplete in TypeScript, with the same field names and docs as Dart.

## Type marshalling

| Dart type           | JSON representation | JS / TS type                         |
| ------------------- | ------------------- | ------------------------------------ |
| `int`, `double`     | number              | `number`                             |
| `String`            | string              | `string`                             |
| `bool`              | bool                | `boolean`                            |
| `List<T>`           | array               | `T[]`                                |
| `Map<String, V>`    | object              | `Record<string, V>`                  |
| `Decimal`           | string              | `string` (annotated `@MoneyString`)  |
| `BigInt`            | string (decimal)    | `string` (annotated `@BigIntString`) |
| `DateTime`          | ISO 8601 string     | `string` (annotated `@IsoDateTime`)  |
| `Uint8List`         | hex string `0x…`    | `string` (annotated `@HexBytes`)     |
| `enum`              | string              | TS string union                      |
| custom freezed type | object              | TS interface                         |

Notes:

* `Decimal` always crosses as a string. **Never floating-point money.** This is a hard rule across the codebase (CLAUDE.md) and the bridge enforces it.
* Hex bytes always carry the `0x` prefix.
* The TS types use string template literal types (e.g. `` `0x${string}` ``) where it gives partners stronger typing without runtime cost.

## Promise / async semantics

* Every `based.*` method returns a Promise on the JS side.
* The Dart side returns `Future<T>`. Synchronous Dart implementations are wrapped: `Future.value(...)` is fine.
* Resolution and rejection are pushed via the bridge's `reply` message.
* Order of reply is **not** guaranteed across different `id`s — callers identify replies by `id`.

## Event subscription

`based.on(topic, callback)` subscribes; `based.off(topic, callback)` unsubscribes. The Dart side maintains a subscription set per mini-app; on dispose, all subscriptions are torn down.

Topics in v1:

| Topic            | Payload                                              | Description                                                                 |
| ---------------- | ---------------------------------------------------- | --------------------------------------------------------------------------- |
| `theme.changed`  | `{ isDark: boolean, colors: Record<string,string> }` | Tenant theme switched; rebuild UI.                                          |
| `locale.changed` | `{ locale: string }`                                 | Device locale changed.                                                      |
| `auth.changed`   | `{ hasUser: boolean }`                               | User logged in/out at host level. Mini-app may need to re-authenticate.     |
| `app.paused`     | `{}`                                                 | The mini-app is being backgrounded. Equivalent to `onPause` lifecycle hook. |
| `app.resumed`    | `{}`                                                 | Foregrounded.                                                               |

## Rate limiting

Rate limits exist to prevent abuse (e.g. flooding `analytics.track`, hammering `signing.signEvm` to nag the user).

| Namespace          | Limit | Window | Notes                                                           |
| ------------------ | ----- | ------ | --------------------------------------------------------------- |
| `signing.*`        | 30    | 60 s   | Confirmation sheets are user-driven anyway; this is a backstop. |
| `tx.*`             | 30    | 60 s   | Same envelope as `signing.*` — counts shared.                   |
| `nav.openDeepLink` | 10    | 60 s   | Prevents redirect-loop attacks.                                 |
| `nav.openMiniApp`  | 5     | 60 s   | Prevents launch-loop attacks.                                   |
| `analytics.track`  | 200   | 60 s   | Generous; partner analytics is fine.                            |
| Global (any call)  | 1000  | 60 s   | Catch-all.                                                      |

When exceeded, the call rejects with `code: "rate_limited"` and a `retryAfter` in details.

## Compatibility shims

When `targetSdkVersion` is older than the host's SDK, the bridge applies shims to keep the mini-app working without partner intervention.

Shim shapes:

* **Renamed param**: old name accepted, mapped to new.
* **Removed param**: silently dropped (with telemetry).
* **Renamed method**: old name routes to new.
* **New required param**: a default is supplied.
* **Removed method**: rejects with `code: "deprecated_removed"` and a deprecation message pointing to the docs. (Only after the deprecation window described in `03-host-contract.md`.)

Shims live alongside the codegen output, in `bridge/compat/v<major>_<minor>.dart`. Each shim has a kill date and a target removal version.

## Threading

Bridge messages flow over isolate `SendPort`s. All marshalling is done on the *sending* side; the receiving side parses and dispatches.

* Dart UI isolate ↔ mini-app isolate uses paired `ReceivePort`s set up at runtime spawn.
* The mini-app isolate runs an event loop that pumps incoming bridge messages into QuickJS, and outgoing JS calls back through the port.

There is no shared memory between isolates. All payloads are JSON. This is the cost of safety; it's also why we cap response sizes (10MB) and avoid pushing image bytes through the bridge — those are referenced by URL and the host renders them.

## What is not in the bridge

These are explicitly handled outside the bridge so the bridge stays minimal and auditable:

* **Image rendering** — the renderer fetches images directly via the host's image cache; the mini-app passes URLs.
* **Animations** — animations are local to the renderer's widget tree; mini-apps describe target states, not per-frame deltas.
* **WebSocket streaming** — v1 omits this. v2 may add a `based.ws.*` namespace; details deferred.

Read [`07-ui-nodes-and-renderer.md`](/docs/integrations/mini-apps-platform/07-ui-nodes-and-renderer.md) next.
