# POS Client

Offline-capable storefront and posclient API


Run the offline-tolerant point-of-sale storefront against the `posclient` backend plugin.

For the POS configuration in the admin UI, see [Sales](https://erxes.io/docs/plugin-sales) and [Point of Sale](https://erxes.io/docs/sales-pos). For gateway headers, see [Authentication & Permissions](https://erxes.io/docs/api-authentication).

## 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`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/posclient_api/src/main.ts) sets up:

- **Express routes** ([`src/routes.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/posclient_api/src/modules/posclient/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/`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/posclient_api/src/worker/index.ts) 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`.

| 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

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.

