Skip to main content

Running FlowPOS Desktop locally with the real merchant UI

This is the operator guide for apps/desktop: how to see frontend-pwa inside the desktop app on your own machine, pair the machine as a print agent, and read what it is doing. It mirrors the mobile guide.

Architecture of what you are launching: Desktop runtime.

How the pieces fit​

The desktop app is a native container written in Rust (Tauri v2). It serves a build of frontend-pwa from the WebView's own app scheme — tauri://localhost on macOS, http://tauri.localhost on Windows — so nothing listens on a socket. The merchant UI is that web build. The container owns the outbox, printing, the station agent, and updates.

LayerSourceWhat a "change" looks like
Backend APIapps/backendNestJS on :4000 locally; Cloud Run in staging/production
Merchant UIapps/frontend-pwaVite on :5173 in a browser. Inside the app it is a built folder: served live by tauri dev's own loopback server (http://127.0.0.1:1430) during development, embedded at compile time in a packaged build
Native shellapps/desktopTauri/Rust. tauri dev compiles it and opens the POS window

VITE_PUBLIC_API_URL is baked into the web build. Rebuilding the shell does not change which API the WebView calls; rebuilding the web build does.

The shipped app does not use this path: installers carry a packaged bundle and later bundles arrive signed from the release channel (feature 054, FR-057…FR-071). This guide is for seeing the UI in the shell on a development machine.

Prerequisites​

RequirementWhy
Node 22, pnpm 10, Rust stableWorkspace baseline; the shell is a Rust crate
Xcode command-line tools (macOS) / WebView2 (Windows)Tauri's WebView
Backend on :4000 (docker-compose up -d, then pnpm --filter backend run start:dev)The packaged UI calls the API; the agent pairs against it
A merchant login with a location and a kitchen stationFor pairing and printing

The backend must allow the app's origin. apps/backend/src/main.ts lists tauri://localhost and http://tauri.localhost as DESKTOP_SHELL_ORIGINS; without them a packaged app renders and every API call is refused. Under tauri dev the origin is the loopback dev server, which a local backend already allows through its private-development rule.

1. Build the web build the shell will serve​

# From the repo root. The API URL is baked in — on the same machine as the backend:
VITE_PUBLIC_API_URL=http://localhost:4000 ./scripts/build-desktop-web.sh

This runs vite build --mode desktop into apps/frontend-pwa/dist-desktop. Desktop mode shares the mobile shell's rules: the service worker is disabled (the shell owns updates) and network-dependent <head> resources are stripped (the build must cold start offline). The script refuses a build that still carries either, prints the API URL it found in the bundle, and nudges cargo so the next shell build re-embeds the folder.

Point the build at another machine's backend with that machine's LAN address, exactly as for the mobile app: VITE_PUBLIC_API_URL=http://192.168.86.61:4000.

2. Run the shell on it​

pnpm --filter desktop run dev:pwa

dev:pwa is tauri dev --config src-tauri/tauri.pwa.conf.json. The override now names the same folder the default config does — apps/frontend-pwa/dist-desktop is the one asset root, for tauri dev and tauri build alike, so a packaged installer carries the merchant UI rather than a placeholder. The override is kept because this command is documented in several places; pnpm --filter desktop run dev opens the same UI. tauri dev serves that folder from a loopback dev server on 127.0.0.1:1430 and opens the POS window on it; the POS window's navigation guard allows the configured dev URL and nothing else outside the app scheme. The first compile takes a few minutes; later ones are incremental.

What a healthy launch looks like:

  • A FlowPOS window opens on the sign-in screen of the merchant UI.
  • A tray icon appears with Open FlowPOS, Status…, Open logs and Quit FlowPOS.
  • Sign in with email and password. Google sign-in opens a popup, which the shell blocks: destinations outside FlowPOS open in the default browser instead.
  • After sign-in, Settings → Printers offers to scan. Compiled connection kinds (network, USB, BLE, serial, and Windows queues on Windows) are advertised in both debug and release builds. flowpos-hwcheck is an optional lab tool; it does not gate Find printers or test print. See Desktop printing.

Closing the POS window does not quit the runtime: it keeps printing for the stations this machine serves. Quit FlowPOS in the tray stops it.

After rebuilding the web build, reopen the POS window (tray → Open FlowPOS) or run dev:pwa again; the dev server reads the folder live.

About → Check for updates is visible inside the shell (the handshake advertises updates.apply). Under tauri dev it reports development and does not fetch a signed bundle — there is no packaged store, and the window is already served from the loopback folder you just built. Packaged installs retrieve {channel}/latest.json from the compiled-in GCS prefix and apply a newer web bundle — newer meaning the published webVersion is not older than the PWA this installer shipped. A latest.json that still names an older deploy is left alone, so Check for updates cannot walk 0.0.384 back to 0.0.383.

Running it on Windows​

The same three steps, with three differences. A Windows 11 ARM64 VM on an Apple-silicon Mac is a supported way to do this — the tcp.windows.json verification record was written on one.

Use Git Bash, or the package scripts. scripts/build-desktop-web.sh is a shell script and pnpm run hands scripts to cmd on Windows, which cannot execute one. The package scripts spell it bash … so one command works on both platforms:

# from apps/desktop
pnpm run build:web # the real merchant UI
pnpm run build:web:stub # an empty asset root, when you only want cargo to compile

Set VITE_PUBLIC_API_URL first, and point it at the Mac's LAN address rather than localhost — inside the VM, localhost is the VM:

$env:VITE_PUBLIC_API_URL = "http://192.168.86.61:4000"
pnpm run build:web

Environment variables are set separately. On Windows, set VITE_PUBLIC_API_URL in PowerShell before pnpm run build:web as shown above. dev:pwa is enough — compiled printer kinds are advertised without FLOWPOS_DEV_UNVERIFIED.

Leftover unverified-kind flags (do not use for Find printers)​

Advertisement is compiled kinds. These flags only mark honesty in the binary:

FlagWhen it appliesEffect
FLOWPOS_DEV_UNVERIFIEDPROFILE=debug onlyWarning + leftover unverified list; does not hide kinds from Find printers
FLOWPOS_UNVERIFIED_RELEASEFLOWPOS_CHANNEL is internal or stagingHandshake unverified_transports; panics if FLOWPOS_CHANNEL=production

Do not set FLOWPOS_UNVERIFIED_RELEASE on production builds. A production installer must not carry a list that claims unverified kinds were shipped as if they were verified. Details: Desktop printing.

Rust needs the ARM64 target if the VM is ARM64: rustup target add aarch64-pc-windows-msvc, plus the Visual Studio Build Tools with the C++ workload. WebView2 ships with Windows 11, so nothing is needed for it.

3. Pair the machine as a print agent​

  1. In the browser PWA (or the POS window), open the kitchen stations screen and generate a pairing code for a station.
  2. Tray → Status…. Enter the API address (http://localhost:4000, or the LAN address if the backend runs elsewhere) and the code, then Pair this machine.
  3. The status window shows the identity, the stations it may claim, its printers, and the permissions the OS has granted.

Set the printer up in the POS window, assign it to the station, and designate this machine as server for the station if the printer is shared. Fire a ticket: it prints once, and the status window's recent section shows it.

Logs and data​

WhatWhere (macOS)
Runtime log folderTray → Open logs (~/Library/Logs/tech.flowandgrow.desktop/)
Outbox, mirror, bundle store~/Library/Application Support/tech.flowandgrow.desktop/
Runtime stderr in devThe terminal running dev:pwa
WebView consoleRight-click in the POS window → Inspect Element (dev builds only)

On Windows the same folders live under %LOCALAPPDATA%\tech.flowandgrow.desktop\.

To start from nothing — unpaired, empty queue — quit the app and delete the Application Support folder.

Where to look when something is wrong​

SymptomLook at
tauri refuses to compile: "frontendDist … doesn't exist"Build the web first: pnpm run build:web (or ./scripts/build-desktop-web.sh). For a cargo-only job that needs no merchant UI, pnpm run build:web:stub
ERR_PNPM_NO_SCRIPT Missing script: dev:pwaThe checkout is on a branch that predates it — main has no desktop scripts. Check out the feature branch
The window shows a "build-time stub" pageThe asset root is the CI stub. Run ./scripts/build-desktop-web.sh for the real one
The UI loads but every call fails with a CORS errorThe backend is not the one you baked in, or it predates DESKTOP_SHELL_ORIGINS
A white window and nothing elseThe POS window was blocked from navigating: the configured devUrl is not the one the guard in bridge/window.rs allows, or the dev server serves nothing at /
The UI still calls an old API address after a tauri buildThe build embedded a stale folder: touch apps/desktop/src-tauri/build.rs and build again
The scan finds nothingOS permission, radio off, USB driver, or paused Windows queue — compiled kinds are advertised without a hardware-verification record. See Desktop printing
About → Check for updates returns error on a packaged buildNeedsNewerShell (install a newer desktop) or retrieve failed; under tauri dev the status is development and nothing is fetched
The shell behaves as a plain browserisInShell() saw no injected bridge — check the init script in bridge/transport.rs