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 outside world crosses this one object — context reads, backend calls, car-status subscriptions.

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 │ callApi()           │
│                │   (a JS object)    │ subscribeCarStatus()│
└────────────────┘                    └─────────────────────┘

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/sdk';

// 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-VIN',
    locale: 'en',
    isDark: false,
    appVersion: '1.0.0',
    appId: 'my-app',
  }),
  callApi: async () => ({ success: true, data: null }),
});

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 / VIN / locale / themeclient.getContext()
callApi(req)Backend HTTP through host's allow-listclient.callApi(req)
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 actuators. No lockDoors(), no setAcOn(), no driving controls. The SDK is read-only by construction. Privileged ops live behind @i99dash/admin-sdk and require explicit user consent + server-side capability tokens — see Privileged mini-apps.
  • 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

sdk-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).
  • callApi() → fixture files in mocks/*.json.
  • Car-status methods → an in-process simulator the control panel drives.

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

On this page