Web fallback
Detect when your page isn't running inside the native app and fall back to a web experience.
Set up once, run anywhere
createBdkNative() returns a client that works inside the native app and in any plain browser. Outside the app, native commands resolve cleanly with triggered: false instead of throwing, so the same build is safe to load everywhere.
In a browser tab outside the app, commands resolve pending (reason: "waiting_for_agent"). With no window at all (SSR, Node, a Web Worker) they resolve skipped (reason: "not_native"). Either way triggered is false — branch on that.
Check if you're in the app
bdk.isNative() returns a boolean synchronously. Use it inline to gate any native-only feature and fall back to a web equivalent.
isNative() can read false for a moment at startup before device info arrives. If you need certainty on the first tick, await ready() (next section) instead.
Check the environment synchronously
bdk.environment() returns "native", "web", or "unknown" without waiting. On current app builds "native" is trustworthy immediately — even at page load — though device info may still be a moment away, so await ready() before reading it. Treat "unknown" as "wait" and fall back to ready(timeoutMs).
Pass assumeWebWithoutSeed: true to createBdkNative() once every installed build of your app is current. A plain browser then reports "web", and ready() resolves null instantly instead of waiting out its timeout.
Environment signals come from the client and can be spoofed — use them to shape UX, never for auth or entitlements.
Wait for the native handshake
bdk.ready(timeoutMs = 0) resolves to BdkDeviceInfo | null and never rejects. A null result means you're outside the native app, so use it to branch your UI.
ready()/ready(0)resolves immediately: the cachedBdkDeviceInfoif present, otherwisenull.ready(timeoutMs)waits up to that many milliseconds for the device to report in, then resolves tonullif it never does.
Check whether a command ran
Every native command resolves a NativeCommandResult — inspect it to know whether the app received the command. triggered === true means it ran; anything else means there's no native shell, so branch on !result.triggered for a web fallback.
See Objects for the full shape. Outside the app a command resolves with triggered: false: in a browser tab it's pending with reason: "waiting_for_agent"; with no window at all it's skipped with reason: "not_native". A rejected command instead throws a BdkError with a code like BDK_NATIVE_UNAVAILABLE, BDK_UNSUPPORTED_VERSION, or BDK_UNSUPPORTED_PLATFORM.
Commands that return data (photos, location, pickers, biometrics, IAP) deliver it on an event — capturePhoto emits photoCaptured, pickPhoto emits photoSelected. Those events never fire in the browser, so always give such calls a non-native path and never block your UI on an event that can't arrive.
Build a web fallback
Decide native vs. web once at startup with ready(), then route every native-only call through an isNative() guard with a browser equivalent behind it. Listeners are safe to attach everywhere — in the browser they simply stay dormant, and a listener that throws is surfaced as BDK_LISTENER_ERROR through onError rather than crashing the page.
Never import @bdk/native/server/* into a browser bundle — those modules are server-only. Use @bdk/native/browser (or @bdk/native) on the client.