Skip to content

Bridging a Web App to a Bluetooth Thermal Printer with a WebView Bridge

Browsers cannot reach a Bluetooth Classic thermal printer, but a native WebView host can. This is how to build a safe JavaScript bridge, generate ESC/POS, and keep the printer connection alive.

By Karani Geoffrey, Founder & CEO, Upeosoft
In short

You host the web app in a native WebView and expose a JavaScript bridge it can call. The web page posts structured receipt JSON to the bridge; the native side validates it, generates ESC/POS bytes, reconnects to the paired Bluetooth Classic printer and prints. Whitelisting the calling host and validating the payload keep the bridge from being abused.

Key takeaways
  • Bluetooth Classic (SPP) printers are unreachable from a browser; Web Bluetooth targets a different profile.
  • A native WebView host can hold the printer connection and expose a JavaScript bridge to the page.
  • The web page passes structured receipt JSON; the native side turns it into ESC/POS commands.
  • Whitelist the calling host and validate the JSON so only trusted pages can trigger prints.
  • Reconnect to the printer before each job, because Bluetooth links drop between prints.
  • Keep printing local and self-contained so it works with no internet and no third-party print app.

The constraint that shapes the whole design

The starting fact is non-negotiable: a web page cannot open a connection to a Bluetooth Classic thermal printer. Those printers use the Serial Port Profile over Bluetooth Classic, and browsers do not expose it. Web Bluetooth, where it exists, only speaks Bluetooth Low Energy, which is a different profile that receipt printers do not implement.

So the architecture is forced: something native has to own the printer connection, and the web app has to reach it through a bridge. The design goal is to make that bridge as thin and safe as possible, so the web app barely changes and the native layer does the printer-specific work. Upeosoft's open-source UpeoRetail Print connector (github.com/Upeosoft-Limited/upeoretail-bluetooth-printer-connector) is a working reference for this pattern, and the sections below trace how it is put together.

The WebView host and the injected bridge

The native app hosts the web application in a full WebView - loading indicators, error recovery, pull-to-refresh, the usual host concerns - so it behaves like a normal app to the user. Into that WebView it injects a bridge object the page can call.

  • The page calls something like window.UpeoRetailPrinter.postMessage(JSON.stringify({ type: 'PRINT_RECEIPT', payload })).
  • The native side receives the string, parses it, and dispatches on the message type.
  • A small helper (a printReceipt() function) can wrap that call so web developers pass a plain receipt object.
  • The bridge is one-directional for printing: page to native. That is all the print path needs.

Guarding the bridge: whitelist and validate

A bridge that any loaded page can call is a liability. Two guards keep it safe. The first is host whitelisting: the native side only honours print messages when the WebView is actually showing a page from an allowed origin. If someone navigates the WebView to an untrusted site, the bridge ignores it. The second is payload validation: before acting, the native side checks the message type and validates the receipt JSON against the shape it expects, rejecting anything malformed or unexpected.

These are not heavyweight controls, but they matter. They turn an open channel into a narrow, predictable one that only trusted pages can use and only for the one thing it is meant to do. In a device that sits on a shop counter, that discipline is what keeps the printer from becoming an accidental attack surface.

From structured JSON to ESC/POS bytes

The web app should not know or care how a thermal printer works. It sends a clean description of the receipt: business header details, itemised lines, totals, and optionally a QR code. The native layer owns the translation into ESC/POS - the command language thermal printers speak - using a generation library and the configured paper width.

Keeping ESC/POS generation on the native side has real benefits. The web code stays portable and printer-agnostic. Supporting both 58mm and 80mm paper becomes a native concern the page never sees. And raw printer bytes never cross the bridge, so the message contract stays clean and safe. The native side lays out the header, aligns the item columns, renders the QR, and appends a paper cut, producing a proper receipt rather than a wall of text.

Local, self-contained, and easy to reason about

The last principle is to keep the printing path entirely local and self-contained. No internet is required to print, no third-party print app sits in the chain, and there are no external dependencies for the ESC/POS generation. The receipt data comes from the page, the bytes are generated on-device, and they go straight to the paired printer over Bluetooth.

That self-containment is what makes the whole thing reliable and debuggable. There are only a few moving parts - WebView host, guarded bridge, ESC/POS generator, Bluetooth service - each with a clear job. It is a small pattern, but it solves a genuinely hard constraint cleanly, and it is the kind of pragmatic engineering we favour: the least machinery that makes the real-world case work. If you need a web app to drive hardware the browser cannot reach, this bridge pattern is a good place to start, and we are happy to help you build or adapt it.

Frequently asked questions

Why use a WebView instead of Web Bluetooth?

Web Bluetooth only speaks Bluetooth Low Energy (GATT). Receipt printers use Bluetooth Classic with the Serial Port Profile (SPP), which the browser cannot access at all. Wrapping the web app in a native WebView lets the native layer - which does have Bluetooth Classic access - own the printer connection, while the web app stays unchanged and simply calls a bridge.

How does the JavaScript bridge actually work?

The native host injects a bridge object into the page, and the web app calls it - for example window.UpeoRetailPrinter.postMessage(JSON.stringify(payload)). The native side receives that string, parses it, validates it, and acts on it. It is a one-way message channel from page to native for the print command, which is all printing needs.

What stops a malicious page from abusing the printer bridge?

Two controls. First, host whitelisting: the bridge only accepts messages when the WebView is showing a page from an allowed origin, so an arbitrary site loaded in the WebView cannot trigger prints. Second, payload validation: the native side checks the message type and the shape of the receipt JSON before it does anything, rejecting anything malformed. Together they keep the bridge narrow and predictable.

Why regenerate the ESC/POS on the native side instead of sending raw bytes from the web?

Because you want the web app to stay simple and portable, and you do not want raw printer bytes crossing the bridge. The page sends a clean, structured description of the receipt - business details, items, totals, optional QR - and the native layer, which knows the printer and paper width, generates the correct ESC/POS. That keeps printer specifics out of the web code and lets you support different paper sizes without changing the page.

Why reconnect before every print?

Bluetooth Classic connections to cheap thermal printers are not durable - they drop when the printer sleeps, moves out of range briefly, or after a period of inactivity. Rather than assume a persistent link, a robust connector re-establishes the connection right before each job. It costs a moment but it means the first print after a quiet spell works instead of silently failing.

Karani Geoffrey
Karani Geoffrey
Founder & CEO, Upeosoft

Karani Geoffrey is the Founder & CEO of Upeosoft, a software and automation company rooted in Kenya. He builds custom software, AI systems, and production-grade ERPNext for businesses across East Africa, and writes about the Kenyan realities - eTIMS, M-Pesa, SHIF, unreliable internet and power - that make or break real systems.

Next step

Want this working in your business?

Upeosoft builds and hardens the systems behind this article - for real Kenyan operations, with eTIMS, M-Pesa and offline realities handled.

Keep reading

Retail and Trade

How to Print Receipts from a Web-Based Sales System to a Bluetooth Thermal Printer

A browser cannot send a receipt straight to a Bluetooth thermal printer. A small connector app bridges that gap, so your web-based sales system prints clean receipts without RawBT, watermarks or workarounds.

6 min readRead article →
For Builders

Building a Reliable SMS-to-Webhook Gateway on Android

Forwarding an SMS to a webhook looks trivial until real conditions hit it: dropped networks, reboots, battery killers, duplicate messages and forgery. This is how to engineer a gateway that survives all of them.

7 min readRead article →
For Builders

Building Offline-First Web Apps: Lessons From Kenya

How to build web apps that keep working when the network does not: service workers, IndexedDB, a durable sync queue, and conflict resolution, drawn from shipping software for real Kenyan connectivity.

8 min readRead article →