What is a mini-app?
The mental model — host, bridge, manifest, bundle. Read once before you start; everything else clicks faster.
In one paragraph
A mini-app is a static web bundle (HTML/JS/CSS) that the i99dash car host loads inside a sandboxed web view. It runs on a head-unit screen mounted in a car. It can read context (which user, which car, which locale, dark mode on/off), it can call backend APIs through a typed proxy, and — if it's a privileged app — it can invoke device-side operations through a separate admin bridge. It can't drive the car or break out of the sandbox.
Architecture
┌─────────────────────┐ ┌───────────────────────────┐
│ your mini-app │ JS │ i99dash host │
│ (HTML / JS / CSS) │──────▶│ sandboxed web view │
│ │ │ + getContext (bridge) │
│ i99dash │ │ + CSP from manifest.network
└─────────────────────┘ └───────────────┬───────────┘
▲ │ plain fetch() to
│ HTML served by │ declared origins
│ ▼
┌──────┴───────────┐ ┌──────────────────────┐
│ i99dash │ │ external HTTPS APIs │
│ /dev-server │ │ (you or 3rd-party) │
└──────────────────┘ └──────────────────────┘Locally, the dev-server attaches the host bridge to your running page
so the same code runs in dev and inside the real host. No
if (dev) { ... } branches in your app.
The five things you need to know
1. Your app is just static files
Anything that compiles to static files works: plain HTML, Next.js with
output: 'export', Vite, Vue, Svelte, anything else. The CLI tarballs
your build directory and uploads it to a CDN. There is no Node runtime
in the head-unit; there is no SSR.
2. The bridge is the contract for host data
Anything you read from the host — context, car signals — goes
through MiniAppClient:
import { MiniAppClient } from 'i99dash';
const client = MiniAppClient.fromWindow();
const ctx = await client.getContext(); // who, where, how
const climate = await client.car.list({ category: 'climate' });You never read window.location for context — the host injects
getContext(). The bridge is the single seam that's testable,
mockable, and stable across host versions.
External APIs are different. To reach an external HTTPS service you
use plain fetch() — the SDK isn't involved — and you declare each
origin in manifest.network so the host's Content-Security-Policy
permits it:
// manifest.json → "network": ["https://api.fuel.example.com"]
const res = await fetch('https://api.fuel.example.com/v1/stations');
const { stations } = await res.json();3. The manifest is your catalog row
manifest.json lives at the project root. Three rules the backend
won't budge on:
idis forever. Pinned home-screen shortcuts on every user's device hold this string. Rotating it orphans every shortcut.versionmust increment on every publish. Same(id, version)is rejected.urlmust be on the host allow-list. Coordinate with ops if you need a custom origin —miniapps.i99dash.appis the v1 default.networkdeclares external egress. Any third-party HTTPS origin your appfetch()es must be listed here, or the host's CSP blocks it. Omit for an app that reaches no external network.iconis bundle-relative, not a URL. Ship the file in your tarball; the publish flow rewrites it to a versioned CDN URL automatically. See App icons recipe.categoryis a closed enum — see Categories + tags for the 10 canonical slugs.
Full reference: MiniAppManifest.
4. There are two clients
| Client | When to reach for it |
|---|---|
MiniAppClient (from i99dash) | Read context, subscribe to car status. Default. (External APIs use fetch(), not the client.) |
AdminClient (from i99dash) | Run device-side ops (pm.*, sys.*, diag.*, fs.*). Restricted distribution. |
If your app only reads context and fetch()es a declared API,
MiniAppClient is the whole story. Most apps stop there. Both clients
ship in the same i99dash package — mini-apps run with full host
capability, no manifest-side gating, and the host arbitrates the actual
call at the device.
5. The dev-server makes it the same as production
The dev-server (run via pnpm dev) boots a tiny web server that
serves your app and attaches the host bridge shim, so getContext()
and the car-status simulator work just like on a real car. External
fetch() calls hit the real origin (dev doesn't enforce the on-car
CSP). Same code path as production: context comes from the bridge,
external data comes from fetch(). You're testing the actual
production code, not a mocked subset.
The dev-server's control panel (/_sdk/ui) lets you toggle driving
state, change the device ID, switch locale, flip dark mode — every input
the real host might inject. Full reference:
Local development.
What a mini-app cannot do
- Actuate the car. No
lockDoors(), nosetAcOn(true). The SDK is read-only by construction. - Persist state on the device. Use
localStoragefor ephemeral UI state if you must, but treat the bundle as if every launch is the first one. - Read other apps' data. Each mini-app is a separate sandboxed web
view; there is no DOM access between apps and no shared
localStorage. - Bypass the manifest.
safeWhileDriving,permissions,minHostVersion— the host enforces every one of them. There is no override.