spoko labels: labels printed straight from the browser

· 6 min read

Printing one label from the inventory took 5 to 10 minutes: an export, a phone, the NIIMBOT app. Now the panel prints it with one button in under 10 seconds, because the browser talks to the printer directly, over Web Bluetooth. An open-source toolkit, built in three days.

Printing one label from the inventory took 5 to 10 minutes: an export, a phone, the NIIMBOT app. Now the panel prints it with one button in under 10 seconds, because the browser talks to the printer directly, over Web Bluetooth. An open-source toolkit, built in three days.

At a glance

Problem
Printing a label from the inventory meant an export, a phone and the NIIMBOT app. 5 to 10 minutes, however many labels.
Solution
The browser talks to the printer directly, over Web Bluetooth. The panel is left with one button.
Result
A label in under 10 seconds, from the phone or the laptop, with nothing exported.
Stack
TypeScript, Web Bluetooth, Filament, Cloudflare Pages.
Time
Three days from the idea to a button in the panel, checked on a real printer from a laptop, a desktop and a phone.

About the project

Every box, shelf and part in the garage inventory carries a QR label. They come out of a NIIMBOT B1, a budget printer under 100 PLN (about €25): Bluetooth, 50×30 mm labels, a battery. It fits in a hand, sits on a shelf between the boxes and has no cable at all. The labels are fine. The trouble was how much had to happen before one came out.

spoko labels is an open-source toolkit that lets a web page print on the B1 directly. It was built so the inventory panel could print a label with one button, and along the way it became a public web app for anyone with the same printer.

The challenge

The B1 only talks over Bluetooth to NIIMBOT’s phone app, and the desktop app connects over a USB cable only. So every label had to go through the NIIMBOT app. At first most of the time went into formatting: fitting the name, the model, the ID and a QR code onto 50×30 mm so it stayed readable. Once there was a template, the data had to be typed in label by label. As the list grew, an xls import took over:

The NIIMBOT phone app: the template editor with a tool label bound to spreadsheet columns, and the grid of tool labels imported from the file
The old way
  1. Step 1

    Export

    Select records in the panel and export them to an xls file.

  2. Step 2

    Transfer

    Get the file onto the phone.

  3. Step 3

    Import

    Import it into the NIIMBOT app and bind the columns to the template.

  4. Step 4

    Print

    Connect to the printer from the app, find the label and print.

Four steps, two devices and 5 to 10 minutes, whether it was one label or twenty. The app has few fonts with Polish characters, and editing on a phone gets tedious past a handful of labels. In practice a label got printed “when there was a moment”, and there never was one. Boxes stayed unlabelled and tidying the inventory stalled.

A NIIMBOT B1 with a long strip of freshly printed box labels: a code like KA-19, the word Pudełko (box), its size and a container number next to a QR codeA printed label on a dark cardboard box: KA-35, Pudełko, 200×160×160, container C0049 and a QR code

How I solved it

Instead of improving the process, I removed it. A browser can speak Bluetooth just like the phone app does; it only needs to know the printer’s language. Nobody documents it, so a Python prototype came first and proved the protocol on a real B1. Then I ported it to TypeScript as four packages and an app, each usable on its own:

Part What it does
labels-core
The printer protocol and the Web Bluetooth connection: printer and battery status, density, copies, calibration, aborting a print.
labels-render
Label templates in millimetres (text, QR code, lines) with fields bound to data, rendered to the image the head prints.
labels-niimbot
Reads templates designed in the NIIMBOT app, so existing designs keep working.
labels-embed
A printer status chip and a print dialog to drop into any web app.
The web app
A built-in template or one imported from the NIIMBOT app, an xlsx file or a JSON API, pick rows, print. English and Polish.
The spoko labels web app: the Tool with QR code template, the inventory's tool list loaded from an xlsx file with two rows ticked, and a pixel-exact preview of a 50 × 30 mm label for an impact driver with a QR code

The preview is the print

The head is 384 dots wide, 8 dots per millimetre, and prints only black and white. A label that looks right on screen and comes out with a smudged QR code is worse than no preview at all, so the renderer works in printer dots:

  • Templates in millimetres, output in dots. The image is thresholded to pure black and white, and QR modules always land on whole dots, so codes scan even at small sizes.
  • The preview shows the hardware. The red bands are outside the head and will not print, the grey corners are outside the rounded label, the dashed line is the safe area. Text that does not fit gets a note before you waste a label.
  • Golden-image tests. A change that moves one dot is a deliberate decision, with before/after images in the pull request.

Bluetooth from a web page

Web Bluetooth has rules, and the project is built around them:

  • The connection starts from a click. The browser only lets you pick a printer in response to a user gesture, so the chip in the print dialog does that first.
  • One command at a time. The printer answers one command before it takes the next. Everything goes through a single queue, so the chip, the dialog and the app never talk over each other.
  • A protocol checked on hardware. Packets are 55 55 CMD LEN DATA… CHK AA AA with an XOR checksum, with status packets the printer sends on its own in between. Every protocol change is checked on a real B1 before it merges.
  • It does not work everywhere, and the app says so. Chrome and Edge on desktop and Android work. iOS and Firefox have no Web Bluetooth. Brave needs a flag, and the app names it.
Printer settings in the web app: calibration offsets across and down in millimetres, with the B1 defaults and a link to the calibration help

One button in the panel

The inventory runs on Filament. The embed turned printing into a single action: the panel passes a template and the selected rows, the dialog renders the preview, the chip holds the connection. The package fetches and sends nothing, so it works under a strict Content Security Policy without exceptions. On a phone the chip collapses to an icon in the top bar. The old print view and the export it relied on are gone.

The inventory panel in production: a product's edit page with the print dialog open, a preview of the label for rear brake lines with the OE number, the item number 0035 and a QR code, copies and density, the connected printer with its battery level, and the Print button

A label printed this way ends up on the part, next to the manufacturer’s sticker:

A product label printed from the panel, stuck on a Bosch ABS pump under the manufacturer's sticker: the OE number 6R0-907-379-AF in bold, the name Pompa ABS Bosch 8.2i, the item number 0019 and a QR code

Printing from the phone works perfectly. I don’t have to export xls files any more. — the workshop, on the first day of printing from the panel

Results

  • A label in under 10 seconds instead of 5 to 10 minutes. Open the record, tap Print label, peel it off. From the phone in the workshop or from the laptop.
  • Nothing exported. The spreadsheet that went stale as soon as a name changed no longer exists. The label always shows what the panel has.
  • Tidying moved again. When a label costs nothing, a box gets one right away, not “when there is a moment”.
  • Data stays in the browser. xlsx files are read on the page, an API is fetched with no credentials, nothing reaches any server.
  • A public app. Anyone with a B1 can print from their own spreadsheet at spoko-labels.pages.dev, and the code is MIT licensed. Anyone who already has templates in the NIIMBOT app points the page at its database file: the templates load in the browser, with a preview and a list of what was simplified, and stay there for the next visit. That is how the inventory’s templates came over.
The web app on two desktop windows and a phone: the upright storage bin template with a QR code in Polish, the connected printer with its battery level, and the same template on a phone

How it was built

From the idea to the button in the panel took three days, 12 to 14 October 2026, in about thirty small pull requests. The port was planned as written specs, one per package. Each package landed with its own tests, 666 of them by the end, and anything that touched the hardware was also checked on a real printer, from a laptop, a desktop and a phone. The validation gate (typecheck, lint, tests, build) runs locally before every merge, and the app is deployed by hand to Cloudflare Pages, with the commit it was built from in the footer.

What is next

  • Template files. Your own template as a JSON file to load, and an imported one to download, so it can move to another app.
  • Deep links, so a link opens the app with a template and a data source already chosen.
  • More printers. Development happens on the B1. Other NIIMBOT models speak a similar protocol, and reports are welcome.

Next step

Is there a process that takes too much of your time?

An export, retyping, moving files between devices, waiting for a program. I build web apps and device integrations that remove or automate that step: here 5 to 10 minutes turned into 10 seconds. Tell me what takes longest where you work, and I will say whether and how it can be made faster.

Let's talk about your process
Back to portfolio