Mobile Printing Foundation
Overview
This feature (spec 050-mobile-printing-foundation) ships no new merchant-facing printing capability. It re-shapes the receipt-encoding, printer-routing, and printer-configuration logic that today lives entirely inside the Print Bridge desktop agent into reusable, transport-agnostic building blocks, and gives printer configuration a central, tenant-scoped home in the database instead of only a per-device local file (config.json).
That is what makes it possible for a future feature to let a phone, tablet, or browser print directly — without one more rewrite of routing/queue logic and without every client re-deriving its own copy of "which printer serves which station." Until that future feature ships, nothing changes for a business currently running Print Bridge: every new code path is inert unless a business/location has explicitly opted in.
Full spec, plan, research, and data model: specs/050-mobile-printing-foundation/.
What was added
@flowpos-workspace/receipt-escpos — a real ./browser export
The Uint8Array-returning encoder path (src/encoder.browser.ts) already existed and was already golden-tested for byte-identity against the Buffer path — it just wasn't a real package export, only reachable via a hand-written Vite alias in apps/frontend-pwa. package.json now declares "./browser" in its exports map, so any consumer (not just a Vite-bundled one) can import { encodeThermalLinesToEscPos } from "@flowpos-workspace/receipt-escpos/browser" and get the exact same bytes as the root Buffer export, with zero change to the root export or its callers.
@flowpos-workspace/printer-transport (new package)
A transport-agnostic port — PrinterTransport / PrinterSession — describing how a print payload is delivered to a physical printer. Ships the port and an error taxonomy only; no TCP/USB/BLE implementation lives in this package. Apps that own a transport (like Print Bridge) implement the port with a thin adapter over their existing code.
@flowpos-workspace/print-orchestrator (new package)
Printer registry, station routing (fan-out — a station may be served by more than one printer), per-printer serialized dispatch queue, a stuck-job watchdog, and the two-layer dedup logic — modelled on Print Bridge's printer-agent.ts + registry.ts. Depends only on printer-transport and its own JobStore port (a notification sink, not a backing store — the queue keeps its own in-memory state).
Corrected during implementation: reading the real Print Bridge code showed it does not retry a failed write locally — one attempt, report the outcome, move on. The only local recovery mechanic is a 30-second stuck-job watchdog. print-orchestrator mirrors that faithfully rather than inventing a retry policy Print Bridge doesn't actually have.
Three new tables (all scoped by business_id + location_id)
| Table | Purpose |
|---|---|
printer_config | Tenant-scoped printer definitions (connection details, display name, station_ids). Station assignment is not exclusivity-constrained — a station may appear under more than one printer. |
printer_discovery_event | Append-only record of a printer-discovery scan (Print Bridge's GET /api/devices/usb), for support/troubleshooting visibility. |
printer_orchestrator_setting | The adoption flag itself. Absence of a row means off — no row is ever auto-created, so every business/location is implicitly on the legacy path. |
Owned by the new apps/backend/src/printer-config/ module (hexagonal — domain/application/infrastructure/interfaces). Two device-guarded endpoints (same DeviceGuard Print Bridge already authenticates with for document-print): GET /printer-config (combined printer list + flag status) and POST /printer-discovery-events.
Print Bridge's additive opt-in path
apps/print-bridge/src/printer-agent.ts gained a periodic check (piggybacking the existing 30s poll cadence) of the adoption flag via server-config-client.ts. When a business/location has opted in, the kitchen-ticket dispatch step routes through a PrintOrchestrator built from tcp-transport-adapter.ts / usb-transport-adapter.ts (thin wrappers over the existing, untouched tcp.ts/usb.ts) instead of calling the transport directly. When not opted in — the default for every existing and new install — the original code path runs byte-for-byte unchanged.
Document/receipt print jobs (routed by receiptPrinter/targetPrinterId, not by station) are not wired to the orchestrator in this feature — that routing model doesn't match print-orchestrator's station-based registry.
Discovery-event reporting is wired into the existing GET /api/devices/usb setup-screen endpoint, gated behind the same adoption flag, so a flag-off install sends no new network traffic at all.
Files that were never modified
Load-bearing for the "zero regression by default" guarantee — if a diff ever touches one of these outside a deliberate, separate decision, something has gone off-plan:
apps/print-bridge/src/printer/tcp.ts,usb.tsapps/print-bridge/src/config.tsapps/print-bridge/src/registry.ts,jobs.ts
Where to look next
- Build on
printer-transport/print-orchestratordirectly: see theircontracts/*.mdin the spec folder and their__tests__/for usage examples against a fake transport. - Add a real printer to the pilot:
specs/050-mobile-printing-foundation/quickstart.mdsteps 5–8 walk through enabling the flag for a test business/location and verifying parity. - The mobile container that consumes this foundation is
051-mobile-app-shell. Generic phone printing is052-mobile-generic-printing. Manufacturer resolution, StarIO10, Android SPP/USB, and cash drawers are053-mobile-vendor-printing— see Vendor printing and Printer Config. - Multi-phase product design:
docs/design/feature-mobile-pos-app.md.