i99dash docs
Concepts

The bridge

What `MiniAppClient` actually wraps, why your code never touches it directly, and how to fake it in tests.

When the i99dash host loads your bundle, it injects a JavaScript object on window called the bridge. Every conversation between your code and the host crosses this one object — context reads and car-status subscriptions. (External HTTPS APIs are not one of those conversations — you reach those with plain fetch(); see Calling an external API.)

You almost never touch the bridge directly. MiniAppClient wraps it with types, timeouts, schema validation, and error wrapping.

The mental model

┌────────────────┐                    ┌─────────────────────┐
│ your code      │                    │ host process        │
│                │                    │ (Flutter app)       │
│ MiniAppClient ─┼──── JS bridge ────▶│ getContext()        │
│                │   window.__i99dash │ subscribeCarStatus()│
│                │   (a JS object)    │ getCarStatus()      │
└────────────────┘                    └─────────────────────┘

The bridge is not a network call. It's an in-process JavaScript object the host populated before your bundle ran. Calls cross the WebView ↔ native boundary inside the host (Flutter platform channels), but from your code's perspective they're plain async function calls.

Why a wrapper?

You could in theory call window.__i99dashHost.getContext() directly. You shouldn't, because MiniAppClient adds:

What the wrapper addsWhy it matters
TypeScript types on every callAuto-complete + compile-time guarantees
Schema validation (zod)Host bumps that drop a field → loud InvalidResponseError instead of silent undefined
10-second default timeout per callHost hangs don't propagate to your UI as forever-spinners
AbortSignal integrationCancel an in-flight call when a user navigates away
Wrapped errors with code + causeSwitch on a stable code; preserve the underlying reason for Sentry
Lazy car-status surfaceNo bridge subscriptions opened until you actually call client.car.*

Reaching past it (e.g., (window as any).__i99dashHost) bypasses all of those. Don't.

Two ways to construct a client

import { MiniAppClient } from 'i99dash';

// Production: read the bridge from window. Throws NotInsideHostError
// if there's no bridge (SSR, Storybook, jsdom).
const client = MiniAppClient.fromWindow();
// Tests / dev / SSR: pass a fake bridge. Any object that satisfies
// the Bridge interface works.
const client = MiniAppClient.withBridge({
  getContext: async () => ({
    userId: 'test-user',
    activeCarId: 'TEST-DEVICE-ID',
    locale: 'en',
    isDark: false,
    appVersion: '1.0.0',
    appId: 'my-app',
  }),
});

The Bridge interface is the contract. As long as your fake matches, the rest of the SDK runs unchanged. See Testing for the full pattern.

Surfaces the bridge exposes

Bridge methodWhat forSDK wrapper
getContext()Read user / device ID / locale / themeclient.getContext()
getCarStatus()One-shot car snapshot (host ≥ 0.0.2)client.car.getStatus()
subscribeCarStatus(cb) / unsubscribeCarStatus(id)Streaming updatesclient.car.onStatusChange(cb)
subscribeCarConnectionState(cb) / unsubscribeCarConnectionState(id)Stream connection up/downclient.car.onConnectionChange(cb)

If your runtime has a Bridge but not a CarStatusBridge (older host, test stub), the streaming methods throw CarStatusUnavailableError — catch it and render a fallback.

What the bridge doesn't expose

  • No network proxy. The bridge does not forward HTTP for you. To reach an external API you declare its origin in manifest.network and call it with plain fetch() — the host enforces the allow-list as a Content-Security-Policy, outside the bridge entirely. See Calling an external API.
  • No actuators on MiniAppClient. No lockDoors(), no setAcOn(), no driving controls. The runtime client is read-only by construction. Privileged ops live behind the admin client exported from i99dash and use reviewed templates.
  • No DOM access between mini-apps. Each mini-app is a separate sandboxed WebView. There's no shared localStorage, no postMessage between them.
  • No persistent storage. Treat each launch as the first one. localStorage works for ephemeral UI state, but the host can clear it at any time.

How the dev-server fits in

The dev-server (run via pnpm dev) injects a bridge shim on window before serving your page. The shim implements the same contract as the real host bridge but routes:

  • getContext() → values from sdk.config.json's dev.context block (overridable live from the control panel at http://127.0.0.1:5173/_sdk/ui).
  • Car-status methods → an in-process simulator the control panel drives.

External fetch() calls are not routed by the dev-server — they go straight to the real origin (the on-car CSP is not enforced in dev), so point your code at a reachable endpoint while developing.

Your code is identical in dev and prod — MiniAppClient.fromWindow() finds either bridge transparently. There is no if (dev) branch.

On this page