Typical scenario: you have a BLE device—a heart rate monitor, smart lamp, or industrial sensor. The manufacturer only provides a mobile app. You want users to control the device directly from your website without installing any software. Web Bluetooth API solves this, but requires careful protocol handling, reconnection logic, and error management. Over 5 years, we have completed more than 20 BLE integrations, from fitness bands to medical monitors. By using the browser, you can save up to 70% on mobile app development and reduce time to market by a factor of three. Typical integration costs $500–$2,000, saving thousands compared to native development. Want to connect a BLE device to your site? We can assess your project in one day—just reach out to us.
Which BLE Devices Can You Connect to a Website?
We connect to heart rate monitors, thermometers, lamps, controllers—any device with Bluetooth Low Energy. The user opens a page, clicks "Connect," selects a device from the list, and the site starts reading sensor data, changing brightness, or turning on a motor. The key is understanding GATT services and characteristics. If documentation exists, integration takes days. If not, we reverse engineer the protocol using a sniffer.
Why Web Bluetooth API Is an Alternative to a Mobile App
Browser-based implementation is three times faster than native development and requires no app store publication. The user doesn't download an app—they simply follow a link. Updates apply instantly on the server, with no versioning. For businesses, this lowers the entry barrier and saves up to 70% on maintaining two platforms.
How to Ensure Stable Connection After a Disconnect
This is critical for devices that need a continuous data stream. We implement automatic reconnection with exponential backoff: first attempt after 1 second, second after 2, third after 4. After three failures, we show a notification. Below is example code in TypeScript.
Connecting to a Device
interface BluetoothDevice {
name: string
gatt: BluetoothRemoteGATTServer
}
async function connectToDevice(serviceUUID: string): Promise<BluetoothRemoteGATTServer> {
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [serviceUUID] }],
})
console.log(`Connecting to: ${device.name}`)
device.addEventListener('gattserverdisconnected', () => {
console.log('Device disconnected')
})
const server = await device.gatt!.connect()
return server
}
Reading Data: Heart Rate Monitor
class HeartRateMonitor {
private server: BluetoothRemoteGATTServer | null = null
private characteristic: BluetoothRemoteGATTCharacteristic | null = null
async connect() {
this.server = await connectToDevice('heart_rate')
const service = await this.server.getPrimaryService('heart_rate')
this.characteristic = await service.getCharacteristic('heart_rate_measurement')
await this.characteristic.startNotifications()
this.characteristic.addEventListener(
'characteristicvaluechanged',
this.handleHeartRateMeasurement.bind(this)
)
}
private handleHeartRateMeasurement(event: Event) {
const value = (event.target as BluetoothRemoteGATTCharacteristic).value!
const flags = value.getUint8(0)
let heartRate: number
if (flags & 0x01) {
heartRate = value.getUint16(1, true)
} else {
heartRate = value.getUint8(1)
}
const rrIntervals: number[] = []
if (flags & 0x10) {
for (let i = 2; i + 1 < value.byteLength; i += 2) {
rrIntervals.push(value.getUint16(i, true) / 1024 * 1000)
}
}
console.log(`Heart rate: ${heartRate} bpm, RR: [${rrIntervals.join(', ')}] ms`)
}
async disconnect() {
await this.characteristic?.stopNotifications()
this.server?.disconnect()
}
}
Writing Data: Controlling a Smart Lamp
class SmartLightController {
private server: BluetoothRemoteGATTServer | null = null
private controlChar: BluetoothRemoteGATTCharacteristic | null = null
private readonly SERVICE_UUID = '00010203-0405-0607-0809-0a0b0c0d1910'
private readonly CONTROL_UUID = '00010203-0405-0607-0809-0a0b0c0d2b11'
async connect() {
this.server = await connectToDevice(this.SERVICE_UUID)
const service = await this.server.getPrimaryService(this.SERVICE_UUID)
this.controlChar = await service.getCharacteristic(this.CONTROL_UUID)
}
async setColor(r: number, g: number, b: number) {
const command = new Uint8Array([
0x33, 0x05, 0x02,
r, g, b,
0x00, 0x00, 0x00,
r ^ g ^ b,
])
await this.controlChar!.writeValueWithResponse(command)
}
async setBrightness(level: number) {
const command = new Uint8Array([
0x33, 0x04,
Math.round(level * 2.55),
0x00,
])
await this.controlChar!.writeValueWithoutResponse(command)
}
async readBatteryLevel(): Promise<number> {
const service = await this.server!.getPrimaryService('battery_service')
const char = await service.getCharacteristic('battery_level')
const value = await char.readValue()
return value.getUint8(0)
}
}
React Hook for Managing Connections
function useBluetooth() {
const [device, setDevice] = useState<BluetoothRemoteGATTServer | null>(null)
const [isConnected, setIsConnected] = useState(false)
const [isSupported] = useState(() => 'bluetooth' in navigator)
const [error, setError] = useState<string | null>(null)
const connect = useCallback(async (serviceUUID: string) => {
try {
setError(null)
const server = await connectToDevice(serviceUUID)
setDevice(server)
setIsConnected(true)
} catch (err) {
if ((err as Error).name === 'NotFoundError') {
setError('No device selected')
} else {
setError((err as Error).message)
}
}
}, [])
const disconnect = useCallback(() => {
device?.disconnect()
setDevice(null)
setIsConnected(false)
}, [device])
return { device, isConnected, isSupported, error, connect, disconnect }
}
Reconnection After Disconnect
device.addEventListener('gattserverdisconnected', async () => {
let retries = 0
while (retries < 3) {
await new Promise((r) => setTimeout(r, 1000 * (retries + 1)))
try {
await device.gatt!.connect()
await resubscribeCharacteristics()
console.log('Reconnected')
return
} catch {
retries++
}
}
console.error('Failed to reconnect')
})
Real-World Case: Fitness Platform Integration
In one project for a fitness tracking startup, we integrated a BLE heart rate monitor that transmitted data every second. The original implementation had a 4-second delay due to inefficient GATT service discovery. By caching service handles and optimizing characteristic notification subscriptions, we reduced the initial connection time to 0.5 seconds and eliminated data lag. The platform now supports real-time heart rate display with automatic reconnection, serving over 10,000 daily active users.
Comparison: Web Bluetooth API vs Native App
| Criteria | Web Bluetooth API | Native App |
|---|---|---|
| Development time | 1–2 weeks | 2–3 months |
| Delivery | Link | App stores |
| Updates | Instant | Manual, via store |
| Platforms | Chrome/Edge | iOS, Android |
| Complexity | Medium | High |
| Cost | From $500 | From $5,000 |
Typical Mistakes in BLE Integration
Device not appearing during scanning. Ensure your site is served over HTTPS and request Bluetooth permission. Often the cause is missing optionalServices. Connection drops when going into background. Use navigator.wakeLock or implement automatic reconnection. Incorrect data parsing. Check bit flags and endianness (little-endian).
Which Browsers Support Web Bluetooth?
Currently, the API works in Chrome 70+, Edge 79+, and Chrome Android. Firefox and Safari do not support it. If your audience uses those browsers, we can suggest workarounds: Native Messaging, PWA with Service Worker, or a hybrid app. For most B2B scenarios, Chrome is the standard choice.
Work Plan and Timeline
| Phase | Duration |
|---|---|
| Document analysis and reverse engineering | 1–3 days |
| Architecture and UI design | 1–2 days |
| Implementation of connections and error handling | 2–4 days |
| Testing on real device | 1–2 days |
| Deployment and monitoring | 0.5–1 day |
Integration with a documented device: 3–5 days. For undocumented devices (protocol reverse engineering): 7–12 days. Cost ranges from $500 to $2,000 depending on complexity and UI needs. We can assess your project within one business day—just contact us. Get a consultation on BLE integration today.
What’s Included
- Full documentation of the protocol and integration code.
- Access to a repository with source code and deployment instructions.
- Training for your team on how to maintain and extend the integration.
- 30-day post-launch support (bug fixes, consultations).
- Guaranteed uptime with automatic reconnection.
- Certified engineers with 5+ years of BLE experience.
With 5+ years of BLE experience and 20+ successful integrations, our team delivers reliable solutions.
Step-by-Step Guide
- Check browser support:
if ('bluetooth' in navigator). - Request the device:
navigator.bluetooth.requestDevice({ filters: [...] }). - Connect to the GATT server:
device.gatt.connect(). - Get the desired service and characteristic.
- Start reading or writing data.
- Handle disconnection: subscribe to the
gattserverdisconnectedevent.
What are GATT services and characteristics?
GATT (Generic Attribute Profile) is a protocol that defines the data structure of a BLE device. A service is a group of characteristics (e.g., "Battery" service). A characteristic is a specific parameter (e.g., battery level). Each service and characteristic has a unique UUID.More about the technology: official documentation and Wikipedia article.







