Skip to main content
The POS system is designed to never block staff from taking orders, regardless of network conditions. Every local action is captured as an idempotent event and safely queued for later delivery.

How offline works

localStorage outbox

nu_pos_react/src/realtime/sync/outbox.ts The outbox is a queue of pending HTTP actions stored in localStorage under the key pos_sync_outbox. It survives browser refreshes and crashes.

Entry structure

Queuing

Entries are deduplicated by pos_reference: if an entry with the same reference already exists, it’s replaced with the newer payload. This prevents duplicate orders. In hub mode, when connection.send() fails, the snapshot is queued as an outbox entry with an _hubSnapshot field.

Flushing

flushOutbound() processes the queue:
  1. Reads all entries from localStorage
  2. Filters entries where nextAttemptAt <= now (respects backoff)
  3. For each entry, sends POST /pos-react/api/orders/{action} to Odoo
  4. On success: removes entry from queue
  5. On failure: increments attempts, computes next retry time, stores error

Retry backoff

After maxRetry attempts (default 8), the entry stays in the queue but won’t be retried until app restart.

Outbox depth monitoring

The transport exposes getOutboxDepth() which returns the current number of pending entries. This is reflected in the sync state machine via OUTBOX_CHANGED events and displayed by the SyncStatusIndicator badge.

Hub-side event log

The hub maintains its own event log for upstream forwarding to Odoo. This is a separate mechanism from the terminal outbox.

Forwarder

nu_pos_hub/src/upstream/forwarder.ts drains the event log to Odoo:
  1. Runs every 5 seconds (FORWARD_INTERVAL_MS)
  2. Queries up to 20 pending events (FORWARD_BATCH_SIZE), oldest first
  3. Non-order events are marked as forwarded immediately (no Odoo action needed)
  4. Order snapshot events are forwarded via the Odoo adapter
  5. On success: marks events as forwarded=1
  6. On first failure: logs error, stops processing batch (preserves ordering)
If Odoo is unreachable, the hub authenticates first, then retries. Events accumulate safely in SQLite.

Idempotency

All order operations are idempotent via pos_reference:
  • Odoo’s create endpoint returns the existing order if the reference already exists
  • The hub’s orderStore.upsert() uses INSERT OR REPLACE keyed by pos_reference
  • The outbox deduplicates by pos_reference before queuing
Re-forwarding the same event (after a crash, restart, or retry) is always safe.

Visibility