Skip to main content

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)​

TablePurpose
printer_configTenant-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_eventAppend-only record of a printer-discovery scan (Print Bridge's GET /api/devices/usb), for support/troubleshooting visibility.
printer_orchestrator_settingThe 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.

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.ts
  • apps/print-bridge/src/config.ts
  • apps/print-bridge/src/registry.ts, jobs.ts

Where to look next​

  • Build on printer-transport/print-orchestrator directly: see their contracts/*.md in 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.md steps 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 is 052-mobile-generic-printing. Manufacturer resolution, StarIO10, Android SPP/USB, and cash drawers are 053-mobile-vendor-printing — see Vendor printing and Printer Config.
  • Multi-phase product design: docs/design/feature-mobile-pos-app.md.