POS Client
Run the offline-tolerant point-of-sale storefront against the posclient backend plugin.
For the POS configuration in the admin UI, see Sales and Point of Sale. For gateway headers, see Authentication & Permissions.
Components
| Piece | Path | Notes |
|---|---|---|
| Backend plugin | backend/plugins/posclient_api | startPlugin name posclient, port 3312, GraphQL + tRPC + subscriptions |
| Storefront app | apps/posclient-front | Standalone Next.js 14 (React 18) PWA via next-pwa, next dev -p 7002 |
There is no frontend/plugins/posclient_ui; the storefront is the standalone app and POS settings live in the Sales UI.
Backend shape
Beyond the standard plugin GraphQL, src/main.ts sets up:
- Express routes (
src/routes.ts):GET /initial-setupandPOST /pl:posclient/initial-setupboth runposInitialSetup. The handler reads the tenant from the request host, loads the POS config, and writes each entry of the?envs={...}JSON query map as a cookie. This is how a kiosk browser receives itsNEXT_PUBLIC_*values when the POS image cannot be rebuilt per site. - Config middleware (
src/configMiddleware.ts): reads theerxes-pos-tokenheader or thepos-config-tokencookie and loads the matchingConfigsdocument intoreq.posConfig. - User middleware (
src/userMiddleware.ts): reads thepos-auth-tokencookie, verifies it againstJWT_TOKEN_SECRET, and setsreq.posUser. - Context (
apolloServerContext):context.configisreq.posConfig; if no token was sent, it falls back to the single non-deleted config when exactly one exists.context.posUsergates user-level operations. - CORS:
credentials: truewith origins compiled fromALLOWED_ORIGINSregexes.
Scheduled sync workers
src/worker/ registers a BullMQ scheduler (posclient-sync-remainder, hourly 0 * * * * UTC) that fans out per tenant (every SaaS organization, or os with the core TIMEZONE config) and, for each POS config with saveRemainder, runs syncRemainders and syncDiscounts against that POS's products.
Setup flow
Configure POS in Sales
Create the POS in the Sales plugin UI; that record produces the POS
tokenand the admin/cashier users the storefront logs in with.Fetch the POS config
From the storefront, sign in and call
posConfigsFetch(token). The resolver pulls the full config (POS settings, admin/cashier users, slots, product groups) from Sales throughGET /pl:sales/pos-init(viaSERVER_DOMAINor the gateway) and stores a local copy inConfigs. A failed fetch deletes the half-written config, so it is safe to retry.Pin the device to that config
posChooseConfig(token)sets thepos-config-tokencookie; subsequent requests loadcontext.configwithout re-sending the token. Thepos-auth-tokencookie fromposLogincarries the signed-in POS user;posLogoutclears the session.Deploy the storefront
The app reads
window.envfrompublic/js/env.js, whichdocker-entrypoint.shregenerates from container env vars on every start;public/js/main.jsthen persists the values tolocalStorageaspos_env_*. For SaaS,getEnv()substitutes a<subdomain>placeholder with the request host whenNEXT_PUBLIC_APP_VERSION=SAAS.
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_MAIN_API_DOMAIN | Gateway base URL for the storefront's GraphQL (/graphql is appended) |
NEXT_PUBLIC_MAIN_SUBS_DOMAIN | WebSocket endpoint for graphql-ws subscriptions, e.g. ws://localhost:4000/graphql |
NEXT_PUBLIC_SERVER_API_DOMAIN | Server-side API base used by the SSR Apollo client (falls back to NEXT_PUBLIC_MAIN_API_DOMAIN) |
NEXT_PUBLIC_SERVER_DOMAIN | Core UI host |
NEXT_PUBLIC_APP_VERSION | OS or SAAS (subdomain substitution) |
Offline and order sync
The storefront is a PWA (registered by next-pwa, disabled in development), so the shell and catalog continue to work when the API is unreachable. Orders taken while disconnected stay local to the POS database as unsynced; a signed-in user runs syncOrders (Settings → sync in the app), which batches up to 100 unsynced paid orders (with items and receipt responses) and pushes them to Sales through the pos.createOrUpdateOrdersMany tRPC procedure. The mutation returns { sumCount, syncedCount }; run it until the counts match. Hourly remainder/discount sync runs on the backend without any client involvement.
Check it works
GET <gateway>/pl:posclient/initial-setup?envs={"NEXT_PUBLIC_APP_VERSION":"OS"}returnssuccessand sets the env cookies.posConfigsFetchcreates aConfigsrow; a second call with a bad token leaves no row behind.- After
posChooseConfig,currentConfigworks without anerxes-pos-tokenheader. - Take an order, disconnect, confirm it stays unsynced, reconnect, and run
syncOrders. Sales shows the order once and subscriptions stream overNEXT_PUBLIC_MAIN_SUBS_DOMAIN.
Troubleshooting
currentConfigreturns "not found": noerxes-pos-token/pos-config-tokenwas sent and more than one non-deletedConfigsrow exists; the single-config fallback only applies when exactly one remains. Re-runposChooseConfigor delete stale configs.- Browser requests blocked by CORS: the storefront origin must match a regex in
ALLOWED_ORIGINS; credentials are required, so wildcard origins do not work. - Env changes ignored after redeploy:
pos_env_*values live inlocalStorage, not the bundle; clear site data or letpublic/js/main.jsrewrite them from a freshenv.js. posConfigsFetchfails immediately: it needs a logged-in POS user (pos-auth-token) and a reachable/pl:sales/pos-init; a partial config is rolled back, so just fix the cause and retry.