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

PiecePathNotes
Backend pluginbackend/plugins/posclient_apistartPlugin name posclient, port 3312, GraphQL + tRPC + subscriptions
Storefront appapps/posclient-frontStandalone 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-setup and POST /pl:posclient/initial-setup both run posInitialSetup. 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 its NEXT_PUBLIC_* values when the POS image cannot be rebuilt per site.
  • Config middleware (src/configMiddleware.ts): reads the erxes-pos-token header or the pos-config-token cookie and loads the matching Configs document into req.posConfig.
  • User middleware (src/userMiddleware.ts): reads the pos-auth-token cookie, verifies it against JWT_TOKEN_SECRET, and sets req.posUser.
  • Context (apolloServerContext): context.config is req.posConfig; if no token was sent, it falls back to the single non-deleted config when exactly one exists. context.posUser gates user-level operations.
  • CORS: credentials: true with origins compiled from ALLOWED_ORIGINS regexes.

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

  1. Configure POS in Sales

    Create the POS in the Sales plugin UI; that record produces the POS token and the admin/cashier users the storefront logs in with.

  2. 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 through GET /pl:sales/pos-init (via SERVER_DOMAIN or the gateway) and stores a local copy in Configs. A failed fetch deletes the half-written config, so it is safe to retry.

  3. Pin the device to that config

    posChooseConfig(token) sets the pos-config-token cookie; subsequent requests load context.config without re-sending the token. The pos-auth-token cookie from posLogin carries the signed-in POS user; posLogout clears the session.

  4. Deploy the storefront

    The app reads window.env from public/js/env.js, which docker-entrypoint.sh regenerates from container env vars on every start; public/js/main.js then persists the values to localStorage as pos_env_*. For SaaS, getEnv() substitutes a <subdomain> placeholder with the request host when NEXT_PUBLIC_APP_VERSION=SAAS.

VariablePurpose
NEXT_PUBLIC_MAIN_API_DOMAINGateway base URL for the storefront's GraphQL (/graphql is appended)
NEXT_PUBLIC_MAIN_SUBS_DOMAINWebSocket endpoint for graphql-ws subscriptions, e.g. ws://localhost:4000/graphql
NEXT_PUBLIC_SERVER_API_DOMAINServer-side API base used by the SSR Apollo client (falls back to NEXT_PUBLIC_MAIN_API_DOMAIN)
NEXT_PUBLIC_SERVER_DOMAINCore UI host
NEXT_PUBLIC_APP_VERSIONOS 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

  1. GET <gateway>/pl:posclient/initial-setup?envs={"NEXT_PUBLIC_APP_VERSION":"OS"} returns success and sets the env cookies.
  2. posConfigsFetch creates a Configs row; a second call with a bad token leaves no row behind.
  3. After posChooseConfig, currentConfig works without an erxes-pos-token header.
  4. Take an order, disconnect, confirm it stays unsynced, reconnect, and run syncOrders. Sales shows the order once and subscriptions stream over NEXT_PUBLIC_MAIN_SUBS_DOMAIN.

Troubleshooting

  • currentConfig returns "not found": no erxes-pos-token/pos-config-token was sent and more than one non-deleted Configs row exists; the single-config fallback only applies when exactly one remains. Re-run posChooseConfig or 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 in localStorage, not the bundle; clear site data or let public/js/main.js rewrite them from a fresh env.js.
  • posConfigsFetch fails 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.
Was this helpful?