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.
| Layer | Source | What a "change" looks like |
|---|---|---|
| Backend API | apps/backend | NestJS on :4000 locally; Cloud Run in staging/production |
| Merchant UI | apps/frontend-pwa | Vite 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 shell | apps/desktop | Tauri/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
| Requirement | Why |
|---|---|
| Node 22, pnpm 10, Rust stable | Workspace 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 station | For 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-hwcheckis 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:
| Flag | When it applies | Effect |
|---|---|---|
FLOWPOS_DEV_UNVERIFIED | PROFILE=debug only | Warning + leftover unverified list; does not hide kinds from Find printers |
FLOWPOS_UNVERIFIED_RELEASE | FLOWPOS_CHANNEL is internal or staging | Handshake 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
- In the browser PWA (or the POS window), open the kitchen stations screen and generate a pairing code for a station.
- 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. - 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
| What | Where (macOS) |
|---|---|
| Runtime log folder | Tray → Open logs (~/Library/Logs/tech.flowandgrow.desktop/) |
| Outbox, mirror, bundle store | ~/Library/Application Support/tech.flowandgrow.desktop/ |
| Runtime stderr in dev | The terminal running dev:pwa |
| WebView console | Right-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
| Symptom | Look 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:pwa | The 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" page | The 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 error | The backend is not the one you baked in, or it predates DESKTOP_SHELL_ORIGINS |
| A white window and nothing else | The 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 build | The build embedded a stale folder: touch apps/desktop/src-tauri/build.rs and build again |
| The scan finds nothing | OS 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 build | NeedsNewerShell (install a newer desktop) or retrieve failed; under tauri dev the status is development and nothing is fetched |
| The shell behaves as a plain browser | isInShell() saw no injected bridge — check the init script in bridge/transport.rs |