Skip to content

Running a server

A minimal server needs three stores:

Store Content Used by
Records the current version of every record: entity, id, hlc, deleted, data merge decisions, the initial pull
Change log every applied change in order: position, entity, id, op, hlc, data, plus the pushing device and user for auditing incremental pulls
Attachments files by SHA-256 with their content type and size attachment endpoints

Plus users, tokens, pairing codes and, if offered, number range counters.

The reference server uses the tables sync_records (unique on entity and record_id, with the log position of the current version), sync_changes (auto-increment seq as log position), attachments (files under storage/app/private/attachments/<first two hex digits>/<sha256>), number_sequences, pairing_codes and Laravel Sanctum's personal_access_tokens (with device_id and device_model). Ids and stamps are stored as upper-case strings rather than native UUID types so that SQLite, PostgreSQL and SQL Server behave the same.

A server holds exactly one inventory, identified by a UUID returned at sign-in. It MUST NOT change while devices are connected. The reference server takes it from LVE_INVENTORY_ID; without it, it derives a UUID (version 5) from APP_KEY, so rotating the application key changes the inventory id. Set LVE_INVENTORY_ID once and keep it.

To serve several inventories, run several servers (or a server with one base URL per inventory). Version 1 has no inventory selection.

Back up records, change log, attachments, users and counters together, at the same moment.

Restoring an older backup moves the server back in time while devices remember a later state:

  • Devices pull with cursors beyond the end of the restored log. The reference server accepts such a cursor and returns nothing until the log grows past it, so devices would silently miss the changes made after the restore. After a restore, either keep the log positions increasing (set the sequence above the highest position ever issued) and tell users to reconnect, or make the server answer old cursors with invalid_cursor so that devices start a fresh initial pull.
  • Devices still hold the newer versions and their outboxes. Records written after the backup come back only when a device pushes them again; the reference app does that when it connects again (it queues every record it holds).
  • Movements are never lost as long as one device still has them.
  • Attachments referenced by records that come back must be uploaded again; devices upload what they hold.
  • Number range counters must not go back: set them above the highest number handed out before the restore, or numbers will be issued twice.

Version 1 keeps everything:

  • Tombstones are never removed. A removed tombstone lets a device that was offline long enough bring the record back.
  • The change log is never compacted. A server MAY compact it (keep only the latest change per record) if it accepts that devices with cursors inside the compacted range must start over; it then answers their cursors with invalid_cursor.
  • Attachments are never removed through the protocol; see Garbage and retention.

The log grows with every change. For an inventory with tens of thousands of movements per year this is a few hundred megabytes of JSON and is not a concern for a relational database.

  • New optional fields need no server change: payloads are relayed as they are.
  • New entity types need no server change: unknown types are stored and relayed.
  • If a server validates payloads, new values of closed sets (a new movement type, a new template column) must be added to its rules before clients use them, or pushes are rejected with invalid_payload. Keep validation to what the server needs.
  • Data migrations on the server (fixing records, filling a field) are server-side writes: write a new version with a stamp greater than the stored one (node 00000000) and append it to the log, so that every device receives it. Never update stored records in place; devices would not see the change.

The protocol is designed for small and medium inventories: tens of devices, thousands of articles, tens of thousands of movements per year.

  • Each device makes a short info, push, pull sequence about once a minute while the app runs. Most pulls are empty.
  • The expensive operation is the initial pull of a new device, which pages through every record.
  • Writes to the change log must be serialised (see Ordering guarantees). The reference server takes one row lock per applied change; this limits throughput to one change at a time, which is ample for the intended size.
  • Pull reads are cheap: an index on the log position (primary key) and one on (entity, id) for the record table.
  • Attachments dominate storage. Put them on a volume that is backed up and that can grow.

A server may project the records into its own tables for reports or for an ERP. Rules for such a read model:

  • Project the current version from the record store, not the change that triggered it, so projecting twice or out of order gives the same result.
  • Accept references to records that do not exist yet; records arrive in any order.
  • Accept duplicate item numbers, location codes and document numbers.
  • Allow negative stock.
  • Never fail a push because the projection failed; the record store is the source of truth and the projection can be rebuilt.

The reference server keeps such a read model for the entities article (with barcodes), location, recipient, movement, delivery_note (with items) and stock levels per article and location, and rebuilds it with php artisan lve:project --fresh. Loans, the company profile and templates are not projected.

Writing back from another system (an ERP that changes article names) works the same way: the system writes server-side changes with greater stamps, and devices receive them with the next pull.