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 adds | Why it matters |
|---|---|
| TypeScript types on every call | Auto-complete + compile-time guarantees |
| Schema validation (zod) | Host bumps that drop a field → loud InvalidResponseError instead of silent undefined |
| 10-second default timeout per call | Host hangs don't propagate to your UI as forever-spinners |
AbortSignal integration | Cancel an in-flight call when a user navigates away |
Wrapped errors with code + cause | Switch on a stable code; preserve the underlying reason for Sentry |
| Lazy car-status surface | No 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 method | What for | SDK wrapper |
|---|---|---|
getContext() | Read user / VIN / locale / theme | client.getContext() |
callApi(req) | Backend HTTP through host's allow-list | client.callApi(req) |
getCarStatus() | One-shot car snapshot (host ≥ 0.0.2) | client.car.getStatus() |
subscribeCarStatus(cb) / unsubscribeCarStatus(id) | Streaming updates | client.car.onStatusChange(cb) |
subscribeCarConnectionState(cb) / unsubscribeCarConnectionState(id) | Stream connection up/down | client.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(), nosetAcOn(), no driving controls. The SDK is read-only by construction. Privileged ops live behind@i99dash/admin-sdkand 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, nopostMessagebetween them. - No persistent storage. Treat each launch as the first one.
localStorageworks 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 fromsdk.config.json'sdev.contextblock (overridable live from the control panel athttp://127.0.0.1:5173/_sdk/ui).callApi()→ fixture files inmocks/*.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.
Related
MiniAppClient— full type signature.Bridge— the interface, for fake bridges.- Testing — how to write
withBridgetests. - Local development — fixture grammar + control panel.