Skip to content

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.

Overview · Ropewalk

The desk surface: dense, keyboard-first, everything bulk. Invoices are raised on the floor and land here.

ImportsUpload a fileThu 17 Sep 2026RA

Articles

2,418

14 categories · articles.v3 template

Customers

1,106

48 trade accounts imported 15 Sep

Warehouses

4

SHOP · BACK · MILL · NGT

Under reorder line

6

All in Kitchen & dining

Open invoices

9

6 from the floor · 3 from the desk

Open invoices newest first

JK move · ↵ open
InvoiceCustomerTotalRaisedWhere raisedPrint
RW-24817Halden Tearooms£248.6017 Sep, 14:05Floor phone 1Printed
RW-24816J. Oduya£38.4517 Sep, 13:51Floor phone 2Held for retry
RW-24815Greyfriars Lettings£612.0017 Sep, 11:20Back officeEmailed PDF
RW-24814The Tallow Room£92.3017 Sep, 10:48Floor phone 1Printed
RW-24812St Aidan's School PTA£156.0016 Sep, 16:10Back officeEmailed PDF
RW-24809Linthorpe Community Kitchen£431.7516 Sep, 12:02Floor phone 3Printed
RW-24806R. Castell£64.9015 Sep, 15:37Floor phone 2Printed
RW-24801Northgate Bakehouse£1,208.4014 Sep, 09:15Back officeEmailed PDF
RW-24797Moss & Kettle Café£187.2012 Sep, 13:26Floor phone 1Printed

Floor phones & printers

Live pairing state
Floor phone 1 · iOSHana Okafor · Printer AConnected14:36
Floor phone 2 · AndroidDev Mistry · Printer BReconnecting14:36
Floor phone 3 · AndroidCallum Reid · —Not paired13:02
RW-24816 held on Floor phone 2. Printer B stopped answering at 13:51; the phone is reconnecting and the job prints when it is back.

Imports · four typed templates

Every file is dry-run before anything is written
TemplateLast fileRowsRunDry run
Articlesarticles.v3autumn-range-articles.xlsx312Today, 09:127 rows failed · nothing written
Customerscustomers.v2trade-accounts-sept.csv4815 Sep, 16:40Passed · written
Warehouseswarehouses.v1northgate-depot.csv12 Sep, 11:05Passed · written
Stock countsstock-counts.v2mill-street-count-0914.csv1,28414 Sep, 18:22Passed · written

Under reorder line

Kitchen & dining
Enamel mug 350ml, rust15 / 40
Oak chopping board, small5 / 12
Cotton tea towel, check24 / 60
Glass storage jar 1L36 / 48
Linen apron, oat7 / 20
Copper measuring cups9 / 16

What the engagement involved

Industry
E-commerce & Retail
Duration
24 weeks
Cooperation model
Fixed price, phased
Services
Back-office engineeringMobile companionHardware integration
Integrations
Bluetooth thermal printersPayment terminalApp Store & Play ConsoleSentry
Technologies
VueCapacitorTypeScriptCordovaNode.jsMySQL
Team
1 Project lead2 Frontend engineers1 Mobile engineer1 QA engineer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

Introduction

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.

What shipped
  • 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
14:38
New invoiceRW-24818 · Floor phone 1 · HanaHO
Printer A · ConnectedTill side · RP-58 thermal · 58mm · ready
Moss & Kettle CaféTrade account · 30 daysChange
Scan or search articles

4 lines

Prices include VAT
Enamel mug 350ml, sageART-10412 · £8.50 each−4+£34.00
Beeswax wood polish 250mlART-10470 · £6.95 each−2+£13.90
Oak chopping board, largeART-10431 · £42.00 each−1+£42.00
Cotton tea towel, stripeART-10457 · £5.50 each−3+£16.50
Net£88.67VAT at 20%£17.73Total£106.40
Raise & print on Printer A
InvoiceInvoicesArticlesPrinter
On screen

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.

Import · ArticlesDry run failed · nothing written

autumn-range-articles.xlsx · 312 rows against articles.v3 · run by Ruth Aldous at 09:12

Write · lockedRe-uploadThu 17 Sep 2026RA
Articlesarticles.v3 · 8 typed columns
Customerscustomers.v2 · 9 typed columns
Warehouseswarehouses.v1 · 6 typed columns
Stock countsstock-counts.v2 · 5 typed columns

Rows read

312

Pass

305

Fail

7

Written

0

One template, one entity, checked column by column. The file is refused whole: fix the 7 rows in the spreadsheet and upload it again.

No half-applied import to clean up: prices and stock are untouched until every row passes.

Failing rows, with reasons

7 of 312 · sorted by row
RowColumnValue in fileWhy it fails
18price_inc_vat£12,50Not a money value: use a point for pence, no £ sign
44barcode5012345678901Check digit should be 0, file has 1
91article_noART-10613Duplicate of row 12 in this file
127vat_rate17.5Not an allowed rate: 20, 5 or 0
203reorder_line-4Must be 0 or more
240categoryCandelsUnknown category: did you mean Candles?
288price_inc_vatemptyRequired column is empty
rowarticle_nonamecategoryprice_inc_vat
17ART-10611Enamel pie dish 24cmKitchen & dining12.95
18ART-10612Enamel pie dish 28cmKitchen & dining£12,50
19ART-10614Pudding basin 1ptKitchen & dining8.50
20ART-10615Pudding basin 2ptKitchen & dining10.50

articles.v3 · typed columns

Not a generic CSV
article_notextRequired · ART-##### · unique in file and catalog1
nametextRequired · up to 80 characters
categoryenumOne of 14 catalogue categories1
barcodeean1313 digits · check digit must match1
price_inc_vatmoneyRequired · pounds with 2 decimals · above 02
vat_rateenum20, 5 or 01
reorder_lineintegerWhole number · 0 or more1
supplier_codetextMust match a supplier on file

Customers, warehouses and stock counts each have their own template and their own checks.

On screen

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.

What shipped
  • A typed template per entity, not one generic importer
  • Dry run first, with failing rows and reasons reported
  • Nothing written until the file passes
Companion 2.14.0 · build 318One commit, both stores

Both store builds from commit a3f9c21 · native shells committed to the repository · built in CI from a clean checkout

Open repositoryThu 17 Sep 2026RA
iOS build 318App Store · productionUploaded
Native shellios/App/ · committed
Commita3f9c21 · main
ArtefactFloor-2.14.0-318.ipa · 18.4 MB
Android build 318Google Play · production trackUploaded
Native shellandroid/app/ · committed
Commita3f9c21 · main
Artefactfloor-2.14.0-318.aab · 11.9 MB

CI run 1,482 · clean checkout, both platforms

Triggered by tag v2.14.0 · no local machine involved

Shared

Fresh checkoutgit clone · a3f9c21Install, exactnpm ci · lockfileBuild webvite build · src/Sync shellsnpx cap sync
iOSArchivexcodebuild · ios/AppSignvault/prod · CI keychainUploadApp Store Connect
AndroidBundlegradle bundleReleaseSignvault/prod · keystoreUploadPlay production track

Native plugins, pinned

Exact versions on both platforms · no ranges
PackageVersioniOS · AndroidUsed for
@capacitor/core"6.1.2"6.1.2 · 6.1.2Shell, bridge
@capacitor/ios"6.1.2"6.1.2 · 6.1.2iOS native shell
@capacitor/android"6.1.2"6.1.2 · 6.1.2Android native shell
cordova-plugin-bluetooth-serial"0.4.7"0.4.7 · 0.4.7Printer pairing and print
@capacitor/preferences"6.0.2"6.0.2 · 6.0.2Paired printer per phone

In the repository

Not generated on a laptop
floor-companion/
src/one Vue codebase
ios/App/committed
App.xcodeprojsigning from CI
android/app/committed
build.gradleversionCode 318
capacitor.config.ts
package-lock.jsonexact versions

One codebase, both stores

On screen

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.

What shipped
  • Native shells committed, not generated locally
  • Pinned plugin versions across both platforms
  • Both stores build in CI from a clean checkout
13:52
PrinterFloor phone 2 · Dev Mistry
Printer B · ReconnectingStopped answering 13:51 · attempt 2, automatic

Where the link is

PairedConnectedSearching

What happened, as it happened

13:51:03Sent RW-24816 · no reply
13:51:05Connection lost · asleep or out of range
13:51:06Reconnecting automatically · attempt 1
13:51:36Attempt 2 · searching for Printer B

Print queue · 1 held

Nothing is dropped
RW-24816 · J. Oduya£38.45 · prints when Printer B is backHeld
Retry now, or print on Printer A

Printers

Printer A · Till side66:12:4F:A0:3C:91Floor phone 1
Printer B · Garden aisle66:12:4F:A0:7E:0BThis phone
InvoiceInvoicesArticlesPrinter
On screen

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.

What shipped
  • 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.

What shipped
  • 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 invoicesProduction

9 open · £3,039.60 · each shows where it was raised: which phone, or the desk

Export CSVThu 17 Sep 2026RA
Production · live invoicesropewalk_prod · credentials vault/prod · only production-channel builds write hereProductionStagingTesting

Open, newest first

/ filter by where raised
InvoiceTotalRaisedWhere raisedPrint
Halden TearoomsRW-24817£248.6017 Sep14:05Floor phone 1 · iOSHana OkaforPrintedbuild 2.14.0 · prod
J. OduyaRW-24816£38.4517 Sep13:51Floor phone 2 · AndroidDev MistryHeld for retrybuild 2.14.0 · prod
Greyfriars LettingsRW-24815£612.0017 Sep11:20Back officeRuth AldousEmailed PDF
The Tallow RoomRW-24814£92.3017 Sep10:48Floor phone 1 · iOSHana OkaforPrintedbuild 2.14.0 · prod
St Aidan's School PTARW-24812£156.0016 Sep16:10Back officeRuth AldousEmailed PDF
Linthorpe Community KitchenRW-24809£431.7516 Sep12:02Floor phone 3 · AndroidCallum ReidPrintedbuild 2.14.0 · prod
R. CastellRW-24806£64.9015 Sep15:37Floor phone 2 · AndroidDev MistryPrintedbuild 2.14.0 · prod
Northgate BakehouseRW-24801£1,208.4014 Sep09:15Back officeRuth AldousEmailed PDF
Moss & Kettle CaféRW-24797£187.2012 Sep13:26Floor phone 1 · iOSHana OkaforPrintedbuild 2.14.0 · prod
9 open invoices£3,039.60

Where they were raised

This list
Floor · 6 · £1,063.20 Desk · 3
Floor phone 1 · iOS3
Floor phone 2 · Android2
Floor phone 3 · Android1
Back office3

Three environments

Separate since week one
ProductionReaches: The shops
ropewalk_prodvault/prodApp Store · Play production
StagingReaches: Office staff only
ropewalk_stagingvault/stagingTestFlight · Play internal
TestingReaches: The printer bench
ropewalk_testvault/testCI artefacts · bench phones
On screen

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Outcome

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.

Process

Phase by phase

  1. 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
  2. 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
  3. 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
  4. 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 · Kitchen & dining6 under reorder line

16 articles across four warehouses · reorder line set per article · last count: Mill Street unit, 14 Sep

Stock counts templateReorder sheet · 6Thu 17 Sep 2026RA
SHOPRopewalk shop floor118 units · 0 articles at zero
BACKRopewalk stockroom251 units · 4 articles at zero
MILLMill Street unit377 units · 3 articles at zero
NGTNorthgate depot140 units · 4 articles at zero

16 articles sorted by article number

Total under the article’s reorder line
ArticleNameSHOPBACKMILLNGTTotalReorderShort byStatus
ART-10412Enamel mug 350ml, sage143660011060—OK
ART-10413Enamel mug 350ml, rust30120154025Under line
ART-10420Stoneware bowl 16cm2248302412450—OK
ART-10431Oak chopping board, large460102012—OK
ART-10432Oak chopping board, small20305127Under line
ART-10457Cotton tea towel, stripe18601204023880—OK
ART-10458Cotton tea towel, check61008246036Under line
ART-10470Beeswax wood polish 250ml92436128130—OK
ART-10488Glass storage jar 1L120186364812Under line
ART-10491Cast iron trivet5142003915—OK
ART-10502Linen apron, charcoal381242720—OK
ART-10503Linen apron, oat104272013Under line
ART-10519Bamboo utensil set72030157224—OK
ART-10526Copper measuring cups24039167Under line
ART-10533Salt pig, white glaze61218104620—OK
ART-10540Wire fruit basket491463318—OK
Units by warehouseSHOP 118BACK 251MILL 377NGT 140Counts land through the stock counts template, dry-run first
On screen

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 it settled

What we'd carry into the next one

04
  1. 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.

  2. 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.

  3. 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.

  4. 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.

New invoiceRW-24818
Printer A · ReconnectingStopped answering · attempt 1, automatic
Moss & Kettle Café4 lines£106.40

Printer A stopped answering. It may be asleep or out of range. The phone is reconnecting by itself.

Print when reconnected

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.

Architecture

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.

  1. 01 · Surfaces
    Vue back office · Capacitor companionA dense, keyboard-driven desk and a one-handed floor app. They share the API and nothing about the interface.
  2. 02 · Ingest
    Typed 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.
  3. 03 · Engine
    Node.js APIOne API behind both surfaces, deployed to production, staging and testing, each with its own credentials.
  4. 04 · State
    MySQL per environmentSeparate databases since week one, so testing a printer integration never touches live invoices.
  5. 05 · Floor
    Bluetooth 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 build
Have a project?

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.