Capability detection
Use `client.has()` and `client.capabilities()` to render conditionally so an older host without a family doesn't crash your UI.
A mini-app uses the host's bridge families at runtime, but the host
on a given car may not ship all of them. A 6-month-old head-unit
firmware and a brand-new build of your app share the same catalog —
yours added vehicle.environment last week, theirs hasn't shipped a
handler for it. Calling client.vehicleEnvironment.getSnapshot() on
that older host throws VehicleEnvironmentUnavailableError.
Capability detection is the pre-flight check that prevents that throw turning into a blank screen.
TL;DR
import { MiniAppClient } from 'i99dash';
const client = MiniAppClient.fromWindow();
// Once, near app startup — memoised internally, cheap to await again.
const caps = await client.capabilities();
const hasMedia = await client.has('media.read');
if (hasMedia) renderNowPlaying(); else renderPlaceholder();client.has(scope) is a memoised predicate over the host's
HostCapabilities. The first call hits the bridge; every later call
is in-process. Cheap enough to await from a render path.
Two capability axes
Capability detection in the SDK spans two independent gates:
| Axis | Authoritative source | Read at runtime via | Failure mode |
|---|---|---|---|
| Host family — does the host implement the bridge handler? | Host build (varies per car ROM) | client.has(scope) / client.capabilities() | <Family>UnavailableError at call time, or false from client.has(scope) |
| Vehicle hardware — does the physical car support this feature? | Per-(DiLink, variant, sub-trim) profile + empirical probe overlay | display.list().vehicle.capabilities | pkg.launch returns not_supported_on_this_car; actuator calls fail at the daemon |
has() only checks the first axis. For the vehicle-hardware
axis, read the snapshot at runtime — see
Vehicle-hardware capabilities
below.
When to use it
Reach for capability detection in three situations:
- Optional features. Your app degrades gracefully without
media.read— render a placeholder instead of crashing. Check at mount, decide once. - Targeting older firmware. You ship an app to a fleet that
spans multiple host versions. Newer scopes (
vehicle.environment,system.read) need ahas()gate; older ones (car.status) don't realistically since every shipping host has them. - Conditional UI affordances. A "now playing" widget, a climate tile, a diagnostics panel — show the section if the family is available, hide it otherwise.
You don't need has() for:
- The React hooks in
i99dash/react— every hook stays on itsfallbackvalue when the family isn't available, no throw.useMedia({ fallback: silentMedia })does the graceful-degradation work for you. - External
fetch()to a declaredmanifest.networkorigin — reaching an external API doesn't depend on a host family capability (only on the CSP allow-list).
The pattern
import { useEffect, useState } from 'react';
import { MiniAppClient, type HostCapabilities } from 'i99dash';
export function AppShell({ client }: { client: MiniAppClient }) {
const [caps, setCaps] = useState<HostCapabilities | null>(null);
useEffect(() => {
client.capabilities().then(setCaps);
}, [client]);
if (!caps) return <Splash />;
return (
<main>
{caps.families.includes('media.read') && <NowPlayingTile />}
{caps.families.includes('climate.read') && <ClimateTile />}
{caps.families.includes('vehicle.environment') && <WeatherTile />}
</main>
);
}capabilities() returns once per client instance. Calling it from
inside each tile component is fine — the second-and-later calls
return the cached value synchronously after the first promise
settles.
Older hosts return bridgeVersion: 'unknown'
Hosts that pre-date the capability handshake (Phase-7-ish and earlier)
don't expose the capabilities() bridge method. The SDK falls back
to a structural sniff:
{ bridgeVersion: 'unknown', families: ['car.status'] }car.status is included when the bridge implements the legacy
car-status methods (getCarStatus + subscribeCarStatus); other
families default to absent. So has('media.read') returns false
on an old host even if the host happens to have a media handler the
SDK can't see — better to render a fallback than to throw mid-render.
When you need the real version string for analytics (e.g. "what
fraction of our user base is on the new bridge?"), branch on
caps.bridgeVersion === 'unknown'.
Don't has() in a tight loop
The first call hits the bridge with a 5-second timeout. The cache is
per-client-instance, so calling has() thousands of times across a
session is fine — but constructing a fresh MiniAppClient per render
defeats the cache. Hold the client in a context, a module, or a hook
that uses useMemo.
// ❌ Wrong — new client per render means a new bridge round-trip
function Tile() {
const client = MiniAppClient.fromWindow();
// ...
}
// ✓ Right — one client, cached caps for the whole session
const client = MiniAppClient.fromWindow();
function Tile() { /* uses the singleton */ }In React, the canonical home is
<MiniAppProvider> +
useClient.
SSR and no-host environments
MiniAppClient.fromWindow() throws NotInsideHostError outside the
host. Use createClientOrSSR
to get null instead:
import { createClientOrSSR } from 'i99dash';
const client = createClientOrSSR(); // null on SSR / Storybook / Node
const hasMedia = client ? await client.has('media.read') : false;Treat "no host" the same as "host without the family" — render the fallback path. The two cases are indistinguishable to a user, and your code stays branch-light.
Vehicle-hardware capabilities
The host shipping your bridge family is necessary but not sufficient
— a Leopard 5 has the pkg.launch family wired up, but its
instrument-cluster panel is daemon-locked, so a pkg.launch to the
cluster role can't actually paint pixels. Vehicle-hardware
capabilities cover that gap.
The set is a closed enum of 16 strings — see
VEHICLE_CAPABILITIES
for the full list. Every entry maps to a fixed bit position so the
host can fold a capability set into a single 32-bit integer for
hot-path subset checks.
import {
VEHICLE_CAPABILITIES,
bitsFromCapabilities,
hasAllCapabilities,
} from 'i99dash';
const need = bitsFromCapabilities(['surface.write.cluster', 'display.read']);
const have = bitsFromCapabilities(vehicle.capabilities); // from display.list
const fits = hasAllCapabilities(have, need); // one ANDReading at runtime
The host emits the active vehicle's capabilities in the vehicle
block of display.list:
const r = await bridge.callHandler('display.list');
// r.vehicle.capabilities → ['display.read', 'pkg.read', ...]
// r.vehicle.capabilityBits → 0b1011 — the same set, packed
// r.vehicle.variantId → 'l5'
// r.vehicle.subTrim → 'flagship' (since SDK 1.8)
// r.vehicle.dilinkFamily → 'di5.0'Use the readable list for logs / UI labels; use the bitmask when you need a fast subset check inside a hot loop.
Profile fallback semantics
The host resolves a 4-tier fallback chain when reading the
empirical-overlay row from the backend
(VehicleCapabilitiesSnapshot):
- Precise —
(dilinkFamily, variantId, subTrim, fingerprint)exact match. - Sub-trim aggregate — same triple, fingerprint stripped.
- Trim aggregate — variant-only, sub-trim stripped.
- DiLink-default — generation-only floor.
When the resolver fell back, the snapshot carries
isFallback: true and a fallbackReason string. Tap-to-launch always
proceeds; the daemon stays the authority for the actual actuator call.
Runtime gating patterns
Read the active vehicle's bits at startup and branch your UI:
- Per-display feature gating. The pkg-launcher example hides its
"Driver" card on cars where
surface.write.clusterisn't in the active vehicle's bits. - Conditional onboarding. "We noticed your car doesn't expose the
passenger panel — here's what changes." Read the bits from the
first
display.listand branch. - Hide-vs-disable. Following the error-model convention, omit controls whose required cap is missing — don't grey them out.
Related
client.capabilities— the SDK reference.HostCapabilities— the host-family wire type.VEHICLE_CAPABILITIES— the canonical hardware-cap list.VehicleCapabilitiesSnapshot— the backend's overlay-resolved row.ProfileKey— the four-tuple identity backend rows are keyed on.- Error model — when
has()is the right tool vs. catch-and-render. - Multi-display guide — uses both axes for picker UX.
- Build for L5 + L8 — where capability detection fits in a resilient build.
- Host catalog status — the catalog-category axis (v5.0 vs v5.1).