A Tauri 2 plugin that exposes native Bluetooth Low Energy operations through a Web Bluetooth-like Rust and TypeScript API.
The desktop backend uses btleplug for discovery, GATT connections, services, characteristics, reads, writes, and notifications. Binary values cross Tauri IPC as Base64 strings.
The implemented backend targets Windows, macOS, and Linux. Mobile commands currently return UnsupportedPlatform; the Android and iOS packages only provide the Tauri plugin registration required by the generated project structure.
Descriptor operations and advertisement watching are not implemented.
Add the Rust plugin to a Tauri host:
[dependencies]
tauri-plugin-web-bluetooth-api = { path = "../tauri-plugin-web-bluetooth-api" }Initialize it with the default first-match device selector:
tauri::Builder::default()
.plugin(tauri_plugin_web_bluetooth_api::init())
.run(tauri::generate_context!())?;Install or link the JavaScript package, then import its public API:
import {
connectGATT,
getPrimaryServices,
onCharacteristicValueChanged,
requestDevice,
startNotifications
} from "tauri-plugin-web-bluetooth-api";
const device = await requestDevice({ acceptAllDevices: true });
const server = await connectGATT(device.id);
const [service] = await getPrimaryServices(device.id);
const characteristic = service?.characteristics[0];
if (service && characteristic) {
await startNotifications(device.id, service.uuid, characteristic.uuid);
const unlisten = await onCharacteristicValueChanged(({ value }) => {
const bytes = Uint8Array.from(atob(value), (character) =>
character.charCodeAt(0)
);
console.log(bytes);
});
// Call unlisten() when the listener is no longer needed.
}The crate exports DeviceSelectionContext, DeviceSelectionHandler, NativeDialogSelectionHandler, and SelectionHandler from its root. Use the built-in selector dialog as follows:
use tauri_plugin_web_bluetooth_api::{
init_with_selection_handler, NativeDialogSelectionHandler, SelectionHandler,
};
tauri::Builder::default()
.plugin(init_with_selection_handler(SelectionHandler::new(
NativeDialogSelectionHandler::new(),
)))
.run(tauri::generate_context!())?;The built-in selector requires withGlobalTauri, event permissions, and access for its generated windows:
{
"app": {
"withGlobalTauri": true
}
}{
"windows": ["main", "web-bluetooth-selector-*"],
"permissions": [
"core:default",
"core:event:default",
"web-bluetooth-api:default"
]
}The plugin registers the internal web-bluetooth-selector:// protocol. Returning Ok(None) from a custom selection handler, closing the built-in dialog, or allowing it to time out reports Error::SelectionCancelled.
The TypeScript bindings expose:
getAvailabilitygetDevicesrequestDeviceconnectGATTanddisconnectGATTforgetDevicegetPrimaryServicesgetCharacteristicsreadCharacteristicValueandwriteCharacteristicValuestartNotificationsandstopNotificationsonCharacteristicValueChangedonGattServerDisconnected
The backend emits these application-wide events:
| Event | Payload |
|---|---|
web-bluetooth-api://characteristic-value-changed |
{ deviceId, serviceUuid, characteristicUuid, value } |
web-bluetooth-api://gattserver-disconnected |
{ deviceId } |
The default permission set grants every command above. Applications should narrow it when they do not require the complete surface.
The repository is a Bun workspace. The Quasar example in examples/quasar-app exercises every command and event, supports filtered discovery and Base64/text/hex values, and uses the native desktop selector.
bun install
bun run build
cargo check
bun run --cwd examples/quasar-app typecheck
bun run --cwd examples/quasar-app lint:check
bun run --cwd examples/quasar-app web:build
bun run example:tauri-devThe Tauri example interacts with the host Bluetooth adapter and nearby peripherals. OS permissions, adapter availability, and peripheral behavior affect runtime results.