Running a server
What a server stores
Section titled “What a server stores”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.
The inventory id
Section titled “The inventory id”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.
Backups and restores
Section titled “Backups and restores”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_cursorso 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.
Retention
Section titled “Retention”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.
Migrations and upgrades
Section titled “Migrations and upgrades”- 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 withinvalid_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.
Scaling
Section titled “Scaling”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,pullsequence 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.
Mirroring into other systems
Section titled “Mirroring into other systems”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.