A back office on the desk, and a receipt printer in the apron pocket
A Vue back office for the people who run the shop, and a Capacitor companion that pairs with a Bluetooth thermal printer so an invoice can be issued anywhere on the floor.
What the engagement involved
- Industry
- E-commerce & Retail
- Duration
- 24 weeks
- Cooperation model
- Fixed price, phased
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
What we were brought in to do
The shop needed two surfaces: a back office where stock, customers and warehouses are managed in bulk, and a phone app that can issue and print an invoice away from the register.
A retailer needing two things that pull in opposite directions: bulk management of articles, customers and warehouses, and the ability to issue and print an invoice on the shop floor away from the register. The first is a desk job with a keyboard and an hour; the second is done standing, one-handed, with a customer waiting.
Full-Stack & Mobile Engineering
Where the old way broke
Bulk data is a desk job and invoicing is a floor job, and one interface built for both ends up bad at each. Bluetooth thermal printers add a second problem: pairing fails in ordinary ways, and an app that hides that leaves a shop assistant tapping print in front of a customer.
The Bluetooth printing was where the previous attempt had failed, and for no subtle reason. Pairing drops in ordinary ways (the printer sleeps, the phone switches, the pairing is lost) and the app hid all of it behind a print button. A shop assistant would tap print three times at a waiting customer and then call support, the worst possible failure surface.
A Vue back office with real import templates for the bulk work, and a Capacitor companion whose printer pairing state is visible at all times, so a failed print is diagnosable by the person holding the phone.
Two surfaces, on purpose
Bulk management is a desk job and invoicing is a floor job; one interface for both would be bad at each.
Bulk catalog work is a desk job with a keyboard and two hours; invoicing is a floor job done standing, one-handed. One responsive interface for both would have been mediocre at each, so there are two: a Vue back office built for density and keyboard work, and a Capacitor companion built for a phone on a shop floor. They share the API and nothing else.
- Two surfaces, each built for its own posture and session length
- Back office tuned for density and keyboard; app for one-handed use
- Shared API, deliberately unshared interface
The invoicing companion on a floor phone: a four-line invoice for a trade customer with a scan-or-search field for articles, VAT shown inside a £106.40 total, and Printer A's connected state pinned under the app bar above a single print button.
A typed import template: a 312-row articles file dry-run against eight typed columns. Seven rows fail with their column, value and reason, the offending cell is shown in context, and nothing has been written.
Typed import templates
Articles, customers and warehouses each get their own typed template.
Articles, customers and warehouses each get their own typed import template with its own columns, validation and error report, where a generic CSV importer would accept anything and fail halfway. Imports dry-run first and report the failing rows with reasons, so the operator fixes the spreadsheet before anything is written instead of cleaning up a half-applied import.
- A typed template per entity, not one generic importer
- Dry run first, with failing rows and reasons reported
- Nothing written until the file passes
One codebase, both stores
One companion release reaching both stores: iOS and Android builds from the same commit, one clean-checkout CI run with a lane per platform, the native plugins pinned to exact versions, and the committed native shells in the repository.
Capacitor with the native shells checked in, so a release doesn't depend on a machine nobody owns.
One Capacitor codebase produces both store builds, with the native shells checked into the repository. A release that depends on a laptop nobody can find is a release that eventually doesn't happen. Native plugins are pinned, and both platforms build in CI from a clean checkout.
- Native shells committed, not generated locally
- Pinned plugin versions across both platforms
- Both stores build in CI from a clean checkout
Printer pairing, always visible: Printer B has stopped answering, the strip says so, the phone is reconnecting on its own and shows each attempt, and the invoice that was sent waits in the print queue to be retried.
Visible pairing
Bluetooth state is always on screen, so a failed print is diagnosable by the person holding the phone.
Bluetooth printer state (searching, paired, connected, error) is on screen at all times, instead of surfacing only when a print fails. That's the difference between a shop assistant who can see the printer has dropped and one who prints three times and calls support. Reconnection is automatic and visible, and a failed job waits in a queue to be retried.
- Pairing state visible at all times, not only on failure
- Automatic reconnection, shown as it happens
- Failed prints queue for retry instead of disappearing
Real environments
Production, staging and testing separated from week one, before any incident could force it.
Production, staging and testing were separated in week one with their own databases, credentials and build channels, so testing a printer integration never touched live invoices. The usual pattern is to retrofit environments after the first incident, and that always costs more than the week it takes at the start.
- Three environments with separate databases and credentials
- Separate build channels, so test builds can't reach shops
- Set up in week one, ahead of any incident
Open invoices in production, and where each one was raised: which floor phone or the desk, who raised it and on which build, with the printed, emailed or held state, beside three environments that each keep their own database, credentials and build channel.
What we built together
- 01
Split the two jobs deliberately: bulk management on the desktop, invoicing on the phone
The two jobs were split deliberately: a dense, keyboard-driven back office and a one-handed companion, sharing the API and nothing about the interface.
- 02
Shipped typed import templates for articles, customers and warehouses in place of a generic CSV importer
Articles, customers and warehouses each get a typed import template with its own validation and a dry run that reports failing rows before anything is written.
- 03
Wrapped the companion with Capacitor so one codebase reaches both app stores
Capacitor produces both store builds from one codebase, with the native shells committed so a release never depends on a particular machine.
- 04
Made Bluetooth pairing state a first-class thing on screen, never a silent failure
Bluetooth pairing state is on screen at all times, with automatic reconnection shown as it happens and failed jobs held in a retryable queue.
- 05
Kept separate production, staging and testing environments from the first week
Separating them in week one cost about a day. Retrofitting environments after an incident is the usual pattern, and it always costs considerably more.
Operational results after launch
1
Codebases for two stores
100%
Print failures diagnosable on device
4
Bulk import formats
3
Environments from week one
Codebases for two stores is a count. Print failures diagnosable on device is a capability claim: pairing state is visible at all times, so a failure has a legible cause. Bulk import formats is a count of typed templates. Three environments from week one is a fact about the setup, not a result.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
Phase by phase
Phase 1: Two Surfaces
Deciding What Belongs Where
Separated the bulk back-office work from the floor work, so each interface is built for its own job and neither is a compromise.
- Surface Split
- Back-Office Scope
- Companion Scope
Phase 2: The Back Office
Bulk Work, Properly
Built the back office in Vue with typed import templates for articles, customers and warehouses, because a generic importer is a support burden dressed as a feature.
- Vue Back Office
- Import Templates
- Warehouse Management
Phase 3: The Companion
One Codebase, Both Stores
Built the invoicing companion with Capacitor so iOS and Android come from one codebase, with the native shells checked in instead of generated on a build machine nobody owns.
- Capacitor App
- iOS & Android Shells
- Invoice Flow
Phase 4: The Printer
Pairing You Can See
Integrated Bluetooth serial printing and put the pairing state permanently on screen, so when a print fails the person holding the phone can tell why.
- Bluetooth Serial Integration
- Pairing State UI
- Print Retry
Stock across four warehouses for sixteen kitchen and dining articles, with the six whose total is under their reorder line called out in amber and the shortfall beside each.
About our collaboration
We built the back office first and the companion second, but the Bluetooth work started in week two on real hardware, never a simulator. Pairing behavior can't be discovered any other way, and everything about the print flow was shaped by what we saw it do.
Phased over twenty-four weeks with the three environments separated in week one, which let the printer integration be tested against real hardware without touching live invoices. The native shells were committed to the repository, because a release that depends on a specific laptop is a release that eventually doesn't happen.
What we'd carry into the next one
- 01
One interface stretched across a desk job and a floor job is bad at both.
The postures are different (sitting with a keyboard versus standing with a customer), and one responsive interface across both is mediocre at each.
- 02
A generic CSV importer is a support burden wearing a feature's clothes.
A generic importer accepts anything and fails halfway, turning a data problem into a support ticket. A typed template refuses the file up front.
- 03
Hardware state belongs on screen. A silent Bluetooth failure becomes a phone call to you.
Hidden hardware state becomes a phone call: the assistant can't see that the printer has dropped, so the only diagnostic available to them is you.
- 04
Checking the native shells into the repo is what keeps releases from depending on one person's laptop.
Committed native shells are the difference between a release process and a person. The laptop version works until that person is on vacation.
One invoice, four printer states
When a print fails, the person holding the phone can see why.
The same invoice on the companion with its printer connected, dropped, reporting an error, and not paired at all. Tap Print as the shop assistant would and see what the phone says and where the invoice goes. Switch tabs, or use the arrow keys once one is focused.
Printer A stopped answering. It may be asleep or out of range. The phone is reconnecting by itself.
Nothing sent yet
What the phone says
Printer A · Reconnecting
Printer A stopped answering. It may be asleep or out of range. The phone is reconnecting by itself.
On screen before anyone taps Print, pinned under the app bar on every screen of the companion.
The invoice, on Print
Held for retry
Held in the queue. It prints when Printer A is back.
Who can fix it
Usually nobody: the phone reconnects on its own. If it doesn't, the assistant can see why: wake the printer or walk back into range.
The previous app, in this state
The same print button, and nothing else on screen. Tap, nothing; tap twice more with a customer waiting; then call support.
Illustrative: the send delay and the pace of reconnect attempts are chosen for the demo, not measured. The states are the ones the companion surfaces: connected, reconnecting after the printer drops, an error the printer reports, and no printer paired.
Two surfaces, one API, and a printer that says what it's doing
Bulk data comes in at the desk and is checked before it lands. Invoices go out from the floor and print over Bluetooth. Both paths run through the same API into an environment that can't be mistaken for another.
- 01 · SurfacesVue back office · Capacitor companionA dense, keyboard-driven desk and a one-handed floor app. They share the API and nothing about the interface.
- 02 · IngestTyped import templatesA template per entity with its own columns and validation. Every file is dry-run, and failing rows come back with reasons before anything is written.
- 03 · EngineNode.js APIOne API behind both surfaces, deployed to production, staging and testing, each with its own credentials.
- 04 · StateMySQL per environmentSeparate databases since week one, so testing a printer integration never touches live invoices.
- 05 · FloorBluetooth serial printingPairing state on screen at all times. Reconnection is automatic and visible, and a failed job waits in a queue to be retried.
Clean stock, legible printing, separate environments
What keeps a bad import or a silent printer away from the counter
A bad file is refused, not half-applied
Articles, customers and warehouses each import through their own typed template. The file is dry-run first and the failing rows come back with reasons, so nothing touches stock or prices until the spreadsheet passes.
A failed print has a cause on screen
Printer state is visible at all times, well before a print fails. Reconnection is automatic and shown as it happens, and a job that didn't print waits in a queue to be retried instead of vanishing.
Tests never reach live invoices
Production, staging and testing have their own databases, credentials and build channels, set up in week one, so printer work was tested on real hardware without a test build reaching a shop.
Running a shop from a desk and a shop floor, or printing to hardware that fails in ordinary ways? Scope your build in three minutes.
Scope your buildNearby engagements
Data & AnalyticsA checkout that stopped losing sales to a 6-second load
Profiling found the real bottleneck behind a slow checkout (an N+1 query and an oversized bundle) and tuned both, with before-and-after metrics locked in as a baseline against future regressions.
E-commerce & Retail · 5 weeks
Web PlatformsA reader people finish, and a library that remembers where they stopped
A reading platform for a comics catalog: a browse surface people can actually navigate, a reader that gets out of the way, and a history that puts everyone back on the page they left.
E-commerce & Retail · 18 weeks
Web PlatformsOne number, a whole handset, and a database that keeps up with 125 brands
A metered IMEI lookup service that turns fifteen digits into a device, its specifications and its status. It's sold three ways to three audiences and backed by a device database that maintains itself.
Consumer Electronics · 22 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














