Quick Start
WebBLE brings the Web Bluetooth API to iOS Safari via a Safari extension and companion app. The canonical web integration is the core auto polyfill, so your existing Web Bluetooth code can stay standard.
Option 1: Core Auto Polyfill (Canonical)
npm install @ios-web-bluetooth/core
import '@ios-web-bluetooth/core/auto';
// Your existing Web Bluetooth code stays standard
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }]
});
On Safari iOS, the package activates the extension-backed polyfill when available. On browsers with native Web Bluetooth support, it stays out of the way.
Option 2: React SDK
npm install @ios-web-bluetooth/react
import { WebBLEProvider, useBluetooth } from '@ios-web-bluetooth/react';
import '@ios-web-bluetooth/core/auto';
function App() {
return (
<WebBLEProvider>
<MyBluetoothApp />
</WebBLEProvider>
);
}
Option 3: Add Install Detection Later
If you need install-state UX, analytics, or a custom missing-extension flow, layer that on after the core path is working.
How It Works
The system has three parts:
- Safari Extension — A free iOS app with a Safari Web Extension that bridges CoreBluetooth to web pages
- Content Script — Injected by the extension, stays dormant by default, and activates the native WebBLE pipeline for the current tab when the user requests it
- Web SDK — Provides the Safari iOS polyfill, optional install detection, and React helpers
When a user visits your site on iOS Safari:
- Your page calls standard Web Bluetooth code
- If the extension is active on Safari iOS, WebBLE exposes
navigator.bluetooththrough the extension bridge - If the extension is missing, you can show your own instructions or add the optional detection package
Zero lock-in. WebBLE polyfills the standard Web Bluetooth API. If the user has Chrome, Edge, or any browser with native support, the SDK stays out of the way. Your code is always standard.
Browser Support
| Browser | Web Bluetooth | WebBLE Action |
|---|---|---|
| Safari iOS 26+ | No (Apple blocks it) | Extension provides full support |
| Chrome 56+ | Native | No-op (native works) |
| Edge 79+ | Native | No-op |
| Firefox | No | Not supported |
@ios-web-bluetooth/core/auto
The core package is the recommended integration surface for production apps that want the standard Web Bluetooth API.
Basic Usage
npm install @ios-web-bluetooth/core
import '@ios-web-bluetooth/core/auto';
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['battery_service'] }]
});
What It Does
- Detects whether the browser already supports Web Bluetooth
- Activates the Safari iOS extension bridge when available
- Polyfills
navigator.bluetoothfor standard Web Bluetooth calls - Keeps your app code aligned with the Web Bluetooth API instead of a custom runtime API
Recommended workflow: verify your device in the live scanner first, then add import '@ios-web-bluetooth/core/auto'; to your app once you know the BLE radio path is healthy.
React SDK
The @ios-web-bluetooth/react package provides React hooks and components on top of the same standard Web Bluetooth model.
npm install @ios-web-bluetooth/react
Provider Setup
Wrap your app with WebBLEProvider after loading the core polyfill:
import { WebBLEProvider } from '@ios-web-bluetooth/react';
import '@ios-web-bluetooth/core/auto';
function App() {
return (
<WebBLEProvider>
<MyBluetoothApp />
</WebBLEProvider>
);
}
Using Hooks
import { useWebBLE, useDevice, useCharacteristic } from '@ios-web-bluetooth/react';
function HeartRateMonitor() {
const { requestDevice, isAvailable } = useWebBLE();
const handleConnect = async () => {
const device = await requestDevice({
filters: [{ services: ['heart_rate'] }],
});
// device is a standard BluetoothDevice
};
return (
<button onClick={handleConnect} disabled={!isAvailable}>
Connect Heart Rate Monitor
</button>
);
}
Available Hooks
| Hook | Purpose |
|---|---|
useWebBLE() | Core context: availability, extension status, requestDevice |
useBluetooth() | Simplified BLE access with auto-detection |
useDevice() | Device connection state, services, connect/disconnect |
useCharacteristic() | Read, write, subscribe to a GATT characteristic |
useNotifications() | Subscribe to characteristic notifications with history |
useScan() | BLE scanning with device list management |
useProfile() | Typed helpers for common BLE device profiles |
Components
| Component | Purpose |
|---|---|
<DeviceScanner /> | Pre-built device scanner UI |
<ServiceExplorer /> | GATT service/characteristic browser |
<ConnectionStatus /> | Connection state indicator |
<InstallationWizard /> | Optional missing-extension guidance UI |
Optional install UX. The React package does not require an API key-first setup. Add the detection package only if you want install-state messaging or a more guided missing-extension flow.
Install Detection
@ios-web-bluetooth/detect is an optional package for extension detection and missing-extension UX. Use it when your product needs install-aware messaging; skip it if the core polyfill is enough.
npm install @ios-web-bluetooth/detect
Programmatic API
import { initIOSWebBLE } from '@ios-web-bluetooth/detect';
await initIOSWebBLE({
operatorName: 'FitTracker',
banner: {
mode: 'sheet', // 'sheet' (default) or 'banner'
dismissDays: 14, // suppress after dismiss
},
onReady() { /* extension installed */ },
onNotInstalled() { /* show fallback */ },
});
Auto-Init (Zero Code)
Import the auto module and configure via meta tags:
<meta name="ioswebble-name" content="My App">
<script type="module">
import '@ios-web-bluetooth/core/auto';
import '@ios-web-bluetooth/detect/auto';
</script>
React Integration
import { IOSWebBLEProvider, useIOSWebBLE } from '@ios-web-bluetooth/detect/react';
function Layout({ children }) {
return (
<IOSWebBLEProvider
operatorName="FitTracker">
{children}
</IOSWebBLEProvider>
);
}
Detection API
isIOSSafari()
Returns true if the current browser is Safari on iOS (including iPad).
import { isIOSSafari } from '@ios-web-bluetooth/detect';
if (isIOSSafari()) {
// Running on iOS Safari
}
isExtensionInstalled()
Returns a Promise<boolean> that resolves after checking for the extension (waits up to 2 seconds for injection).
import { isExtensionInstalled } from '@ios-web-bluetooth/detect';
const installed = await isExtensionInstalled();
initIOSWebBLE(options)
Main entry point. Detects iOS Safari, checks extension state, and lets you hook in custom install guidance if needed.
| Option | Type | Description |
|---|---|---|
operatorName | string? | App name for install prompt |
banner | object | false | Install guidance configuration or false to disable |
onReady | () => void | Called when extension is detected |
onNotInstalled | () => void | Called when extension is NOT installed |
Install UX Options
Configure how missing-extension guidance appears when you use the optional detection package.
| Option | Type | Default | Description |
|---|---|---|---|
mode | 'sheet' | 'banner' | 'sheet' | Bottom sheet (iOS-native) or lightweight banner bar |
position | 'top' | 'bottom' | 'bottom' | Bar position (banner mode only) |
text | string | Auto-generated | Custom banner text |
buttonText | string | 'Start Setup' | CTA button text |
operatorName | string | Page title / hostname | Your app name in the prompt |
startOnboardingUrl | string? | Current page | Preferred setup or help URL for the CTA |
appStoreUrl | string? | Legacy fallback | Legacy CTA destination override; supported but not recommended as the primary path |
dismissDays | number | 14 | Days to suppress after dismiss |
style | Record<string, string> | — | Custom CSS for banner bar mode |
React Hooks
useWebBLE()
Returns the core WebBLE context. Must be used inside <WebBLEProvider>.
| Property | Type | Description |
|---|---|---|
isAvailable | boolean | Whether Bluetooth is available |
isExtensionInstalled | boolean | Whether the WebBLE extension is detected |
isLoading | boolean | Whether detection is in progress |
isScanning | boolean | Whether a BLE scan is active |
devices | BluetoothDevice[] | Discovered devices |
error | Error | null | Last error |
requestDevice() | function | Request a BLE device (standard API) |
getDevices() | function | Get previously paired devices |
requestLEScan() | function | Start a BLE scan |
stopScan() | function | Stop the current scan |
useIOSWebBLE()
From @ios-web-bluetooth/detect/react. Returns detection state for optional install-aware UI.
| Property | Type | Description |
|---|---|---|
isInstalled | boolean | null | Extension installed (null while detecting) |
isDetecting | boolean | Whether detection is still running |
isIOSSafari | boolean | Whether on iOS Safari |
Events
The SDK dispatches custom events on the window object:
| Event | When |
|---|---|
ioswebble:ready | Extension detected and ready |
ioswebble:notinstalled | Extension NOT installed on iOS Safari |
webble:extension:ready | Extension content script injected (from extension itself) |
// Listen for extension detection
window.addEventListener('ioswebble:ready', () => {
console.log('Extension is active!');
});
Background Sync
iOS-only. Background Sync runs through the WebBLE companion app, and notification-based workflows require notification permission. Call requestPermission() before registering characteristic alerts or beacon scans.
Use navigator.webble.backgroundSync to keep approved BLE devices active while Safari is backgrounded and to deliver iOS notifications for important BLE events.
requestPermission()
Call this from a direct user gesture. It resolves to 'granted', 'denied', or 'prompt'.
document.querySelector('#enable-background-sync')?.addEventListener('click', async () => {
const permission = await navigator.webble.backgroundSync.requestPermission();
if (permission !== 'granted') {
console.log('Background notifications not enabled:', permission);
return;
}
console.log('Background notifications enabled');
});
Background Connection
requestBackgroundConnection({ deviceId }) asks the companion app to keep a granted device connected in the background. It is silent by design and does not show an iOS notification on its own.
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }]
});
await device.gatt.connect();
await navigator.webble.backgroundSync.requestBackgroundConnection({
deviceId: device.id
});
Characteristic Notifications
registerCharacteristicNotifications(options) fires an iOS notification when a characteristic changes while Safari is backgrounded.
condition is required and is evaluated natively before iOS notification delivery.
| Option | Type | Description |
|---|---|---|
deviceId | string | Device ID from a previous requestDevice() call in the current tab |
serviceUUID | BluetoothServiceUUID | Service containing the characteristic to watch |
characteristicUUID | BluetoothCharacteristicUUID | Characteristic to watch for value changes |
condition | NotificationCondition | Native condition that gates notification delivery |
template | NotificationTemplate | Notification content shown by iOS |
replyAction | ReplyActionConfig? | Optional inline reply configuration |
cooldownSeconds | number? | Minimum seconds between notifications for the same registration (default 5) |
NotificationTemplate
| Property | Type | Description |
|---|---|---|
title | string | Notification title. Supports placeholders |
body | string | Notification body. Supports placeholders |
url | string | HTTPS URL opened when the user taps the notification |
sound | boolean? | Play the default notification sound. Defaults to true |
| Placeholder | Meaning |
|---|---|
{{device.name}} | Device name |
{{device.id}} | Device ID |
{{value.hex}} | Characteristic value as lowercase hex |
{{value.utf8}} | Characteristic value decoded as UTF-8 |
{{value.int16be}} | First two bytes as signed big-endian int16 |
{{value.int32be}} | First four bytes as signed big-endian int32 |
{{timestamp}} | ISO 8601 timestamp for the received value |
ReplyActionConfig
| Property | Type | Description |
|---|---|---|
actionTitle | string | Inline reply button label |
placeholder | string? | Input placeholder shown in the reply sheet |
Use friendly text here for forward compatibility. The initial release uses the standard iOS inline reply UI.
const messageAlerts = await navigator.webble.backgroundSync.registerCharacteristicNotifications({
deviceId: device.id,
serviceUUID: '12345678-1234-1234-1234-123456789abc',
characteristicUUID: '87654321-4321-4321-4321-cba987654321',
condition: {
decode: 'uint8',
operator: 'changed',
threshold: 0
},
cooldownSeconds: 15,
template: {
title: 'Message from {{device.name}}',
body: '{{value.utf8}}',
url: 'https://example.com/messages',
sound: true
},
replyAction: {
actionTitle: 'Reply',
placeholder: 'Send a quick response'
}
});
Beacon Scanning
registerBeaconScanning(options) matches BLE advertisements and delivers an iOS notification when a filter hits.
| Option | Type | Description |
|---|---|---|
filters | BLEScanFilter[] | One or more advertisement filters. At least one filter must include services |
cooldownSeconds | number? | Minimum seconds between notifications for the same detected device (default 5) |
template | NotificationTemplate | Notification content shown by iOS |
cooldownSeconds throttles how often iOS shows a notification. It does not control scan frequency.
BLEScanFilter
| Property | Type | Description |
|---|---|---|
services | BluetoothServiceUUID[]? | Service UUIDs to match in advertisements |
namePrefix | string? | Optional device or advertisement name prefix |
Fast catch-up. Background beacon delivery on iOS is designed for quick catch-up rather than fixed real-time discovery. Use service UUID filters and sensible cooldowns for the most reliable alerts.
const beaconRegistration = await navigator.webble.backgroundSync.registerBeaconScanning({
filters: [
{ services: ['feaa'] },
{
services: ['12345678-1234-1234-1234-123456789abc'],
namePrefix: 'Sensor-'
}
],
cooldownSeconds: 60,
template: {
title: 'Nearby BLE beacon',
body: '{{device.name}} is advertising nearby',
url: 'https://example.com/beacons',
sound: true
}
});
Managing Registrations
Use getRegistrations(), unregister(id), and update(id, template) to manage registrations for the current origin.
const registrations = await navigator.webble.backgroundSync.getRegistrations();
for (const registration of registrations) {
console.log(registration.id, registration.type);
}
await navigator.webble.backgroundSync.update(messageAlerts.id, {
title: 'New device message',
body: '{{value.utf8}}'
});
await navigator.webble.backgroundSync.unregister(beaconRegistration.id);
BackgroundRegistration handle
| Property / Method | Type | Description |
|---|---|---|
id | string | Unique registration ID |
type | 'connection' | 'characteristic-notification' | 'beacon-scan' | Registration kind |
createdAt | number | Registration creation timestamp in Unix milliseconds |
lastTriggeredAt | number? | Last delivery timestamp, when available |
unregister() | () => Promise<void> | Remove this registration |
update() | (template: Partial<NotificationTemplate>) => Promise<void> | Update title, body, URL, or sound |
Security notes
template.urlmust usehttps://and is only opened when it matches the same origin as the page that registered it.deviceIdis scoped to devices previously granted viarequestDevice()for the current site flow.- Template values are sanitized before iOS renders the notification, and only supported
{{...}}placeholders are interpolated.
Complete example
const permission = await navigator.webble.backgroundSync.requestPermission();
if (permission !== 'granted') {
throw new Error('Enable notifications to use background BLE alerts');
}
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['heart_rate'] }]
});
await device.gatt.connect();
const keepAlive = await navigator.webble.backgroundSync.requestBackgroundConnection({
deviceId: device.id
});
const heartRateAlerts = await navigator.webble.backgroundSync.registerCharacteristicNotifications({
deviceId: device.id,
serviceUUID: 'heart_rate',
characteristicUUID: 'heart_rate_measurement',
condition: {
decode: 'uint8',
operator: 'gt',
threshold: 120
},
cooldownSeconds: 30,
template: {
title: 'Heart rate update',
body: '{{device.name}} sent {{value.hex}} at {{timestamp}}',
url: 'https://example.com/monitor',
sound: true
}
});
const activeRegistrations = await navigator.webble.backgroundSync.getRegistrations();
console.log(activeRegistrations.map(({ id, type }) => ({ id, type })));
await heartRateAlerts.update({
body: 'Latest payload from {{device.name}}: {{value.hex}}'
});
// Later, when you no longer need background delivery:
await heartRateAlerts.unregister();
Troubleshooting
Extension not detected after installation
The user may have enabled the extension but not granted website permissions. They need to:
- In Safari, tap aA in the address bar
- Tap the WebBLE icon (may have a warning badge)
- Choose "Always Allow"
- Then "Always Allow on Every Website"
"Allow for One Day" expires silently. If a user chose "Allow for One Day" instead of "Always Allow," the extension will stop working the next day with no notification. Guide users to "Always Allow" in your onboarding.
Extension works on some sites but not others
The user may have chosen per-site permissions. They need to grant "Always Allow on Every Website" for consistent behavior.
HTTPS required
Web Bluetooth requires a secure context. Ensure your site uses HTTPS (or localhost for development).
navigator.bluetooth is undefined
- On iOS Safari: the extension is not installed or not granted permissions
- On Firefox: Web Bluetooth is not supported
- On Chrome/Edge: should work natively — check
chrome://flags/#enable-experimental-web-platform-features
Privacy
WebBLE is designed with privacy as a core principle:
- All BLE data processed locally. Bluetooth communication happens entirely on-device between the browser and CoreBluetooth. No data is ever proxied through a server.
- No BLE data leaves the device. Bluetooth communication stays on-device between Safari, the extension, and CoreBluetooth. WebBLE does not proxy device traffic through a server.
- No browsing history is stored or transmitted for BLE functionality. The extension uses Safari page integration to expose the API, but WebBLE does not inspect, retain, or transmit page content or browsing history to deliver Bluetooth connectivity.
- Optional analytics stay minimal. When a developer explicitly enables SDK analytics, the service records limited install-state events such as hostname, user agent, timestamp, and the integration identifier tied to that developer account.
- Proprietary product, public docs. WebBLE publishes detailed setup and API documentation, but the product should be evaluated as proprietary software today.
"Always Allow on Every Website" does NOT mean "always watching." It means the extension is available on every website, but only activates when a site explicitly requests Bluetooth access via the Web Bluetooth API.