Install and operate
The install is one server process: the JSON API, the components, the admin page and these pages. Its state (layouts, waves, reservations, scans) lives in a store: Redis for production, or memory for a trial.
Install#
The executable#
The download has one archive per platform (Linux x64 and arm64, macOS, Windows), each with a single executable that needs no Node.js or other runtime. Unpack it and start it:
tar -xzf slotting-1.0.0-linux-x64.tar.gz
cd slotting-1.0.0-linux-x64
REDIS_URL=redis://redis.internal:6379 ./slottingIt listens on http://127.0.0.1:10083 (set HOST and PORT to change that) and logs to standard output. Open
http://127.0.0.1:10083/ for the admin page and /docs/ for these pages. Run it under your service manager (systemd,
Windows service wrapper, launchd) so it restarts after a failure or a reboot. It writes nothing to disk.
On Linux the executable needs glibc 2.28 or later, libstdc++ and libatomic, which minimal installs and slim images
often lack (the program then exits at once with "error while loading shared libraries: libatomic.so.1"). Install it
first: sudo apt-get install -y libatomic1 on Debian and Ubuntu, sudo dnf install -y libatomic on RHEL and Amazon
Linux. The archive's Dockerfile installs it.
Docker#
The Linux archives contain a Dockerfile that builds an image from the executable:
cd slotting-1.0.0-linux-x64
docker build -t slotting:1.0.0 .
docker run -d --name slotting -p 127.0.0.1:10083:10083 \
-e REDIS_URL=redis://redis.internal:6379 \
-e LICENSE_FILE=/run/secrets/slotting-license -v /etc/slotting/license.txt:/run/secrets/slotting-license:ro \
-e SLOTTING_INSTANCE_ID=slotting-1 \
slotting:1.0.0In the image the server listens on 0.0.0.0:10083. Publish the port only on a private interface or network (above:
127.0.0.1 of the host), never on a public one. The container is stateless and works with --read-only. Set
SLOTTING_INSTANCE_ID, because a container's hostname changes every time it is recreated (License).
Check the install#
curl -s http://127.0.0.1:10083/healthz # {"status":"ok","store":"redis","storeOk":true}
curl -s http://127.0.0.1:10083/v1/info # version, license, photo import/healthz answers 503 with "status": "degraded" and "storeOk": false while Redis can't be reached; use it for your load
balancer's and monitoring's health check. The Status page shows the same in a browser.
Configuration#
Everything is configured with environment variables. All are optional: without any, the install runs on 127.0.0.1:10083
with the memory store and the evaluation license.
| Variable | Default | |
|---|---|---|
PORT |
10083 |
The HTTP port |
HOST |
127.0.0.1 |
The interface to listen on. 0.0.0.0 for all interfaces (only on a private network) |
LICENSE_KEY |
Your license key (one line starting with SL2.) |
|
LICENSE_FILE |
Or: the path of a file that holds it, for example a mounted secret. See License | |
LOG_LEVEL |
info |
debug, info, warn or error |
SLOTTING_INSTANCE_ID |
derived from the hostname | This installation's id, shown on the Status page. 1 to 128 of A-Z a-z 0-9 . _ : - |
SLOTTING_STORE |
redis when a Redis setting is set, else memory |
memory or redis |
REDIS_URL |
One Redis server: redis://host:6379, rediss://host:6380 for TLS. User and password may be in the URL (redis://user:password@host:6379/0); the path may only be a database number |
|
REDIS_PASSWORD |
The password, if it isn't in the URL (also used for cluster and sentinel nodes) | |
REDIS_TLS_CA_FILE |
the system's CAs | A PEM file with the CA certificate of your Redis (private CA). Needs rediss:// URLs |
REDIS_CLUSTER_NODES |
Redis Cluster: a comma-separated list of node URLs (rediss://n1:6379,rediss://n2:6379). Instead of REDIS_URL |
|
REDIS_SENTINELS |
Redis Sentinel: a comma-separated list of sentinels (host:26379 or URLs). Instead of REDIS_URL |
|
REDIS_SENTINEL_NAME |
The name of the master the sentinels monitor. Required with REDIS_SENTINELS |
|
REDIS_KEY_PREFIX |
slotting: |
Prefix of every key (1 to 64 of A-Z a-z 0-9 . _ : -). Give separate installs on one Redis different prefixes |
REDIS_LOCK_WAIT_MS |
5000 |
How long a request waits for a layout that another request is changing, 100 to 60000, before it answers 503 store_busy. REDIS_LOCK_TIMEOUT_MS is its old, deprecated name |
REDIS_LOCK_TTL_MS |
15000 |
How long a layout's lock lives if the instance holding it stops, 1000 to 600000. The holder keeps extending it while it works; keep it well above the longest wave planning |
REDIS_COMMAND_TIMEOUT_MS |
2000 |
How long a request waits for Redis to answer, 100 to 60000, before it answers 503 store_unavailable (a hung or overloaded Redis) |
SLOTTING_WAVE_RETENTION_HOURS |
72 |
Closed waves older than this are deleted when the next wave is created on their layout |
SLOTTING_CORS_ORIGINS |
none | Comma-separated origins (https://wms.example.internal) whose pages may call /api and /v1/info from the browser. Needed only when the components run on another origin than the install (Embedding) |
SLOTTING_ALLOWED_HOSTS |
any | Comma-separated host names, without port (slotting.internal.example,10.0.4.12), that the install answers to. Requests with any other Host header get 421 host_not_allowed. Protects against DNS rebinding; recommended in production. Include the names your health checks use (127.0.0.1, localhost) |
SLOTTING_COMPACT_MAX_WAIT |
3 |
Strict compactness: after how many releases an order that waits for a compact group of bins takes the best one it can get, 0 to 1000 (why). Read when a wave is created |
SLOTTING_LIGHTS |
none |
Put-to-light: none, http, mqtt or simulator. The driver's settings (SLOTTING_LIGHTS_URL, SLOTTING_LIGHTS_TOKEN, SLOTTING_LIGHTS_MQTT_URL, SLOTTING_LIGHTS_MQTT_USERNAME, SLOTTING_LIGHTS_MQTT_PASSWORD, SLOTTING_LIGHTS_MQTT_CA_FILE, SLOTTING_LIGHTS_MQTT_PREFIX, SLOTTING_LIGHTS_RESYNC_MS) are in Put-to-light |
SLOTTING_WEBHOOK_URLS |
Webhooks: the URLs that receive the events, with SLOTTING_WEBHOOK_SECRET (required), SLOTTING_WEBHOOK_EVENTS, SLOTTING_WEBHOOK_TIMEOUT_MS, SLOTTING_WEBHOOK_MAX_ATTEMPTS (default 20, about 8 hours of retries), SLOTTING_WEBHOOK_POLL_MS and SLOTTING_OUTBOX_MAX_PER_LAYOUT (default 100000). Without URLs no events are kept. See Webhooks |
|
SLOTTING_AI_PROVIDER |
Photo import: anthropic, bedrock, openai, azure-openai, google or openai-compatible (which settings each needs) |
|
SLOTTING_AI_MODEL |
claude-opus-5-5 for anthropic |
The vision model. Required for the other providers (for Azure: the deployment) |
SLOTTING_AI_API_KEY |
The provider's API key. Falls back to ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY or AZURE_OPENAI_API_KEY |
|
SLOTTING_AI_BASE_URL |
the provider's | The endpoint: required for openai-compatible (http://ollama.internal:11434/v1), optional for the others (a proxy or gateway) |
SLOTTING_AI_TIMEOUT_MS |
60000 |
Time limit of one photo request to the provider, 1000 to 600000 |
AZURE_OPENAI_ENDPOINT |
Azure OpenAI: https://<resource>.openai.azure.com |
|
AZURE_OPENAI_DEPLOYMENT |
Azure OpenAI: the deployment of a vision model | |
AZURE_OPENAI_API_VERSION |
2024-10-21 |
Azure OpenAI: the API version |
AWS_REGION |
the SDK's default | Bedrock: the region. Credentials come from the default AWS chain (environment, shared config, instance or task role) |
Settings that can't work (an unknown SLOTTING_STORE, a malformed Redis URL, two Redis modes at once, an unreadable
REDIS_TLS_CA_FILE or SLOTTING_LIGHTS_MQTT_CA_FILE, a webhook URL without a secret) stop the install at startup with a
message. Passwords and keys never appear in the log or in any
response.
Showcase and demo limits#
For a public demo or a shared evaluation install, a few settings cap what visitors can create. They are all off unless
set, and a production install doesn't need them. A request beyond a cap answers 409 limit_reached, with the setting in
details.limit and its value in details.max. A value that can't work stops the install at startup.
| Variable | Default | |
|---|---|---|
SLOTTING_MAX_LAYOUTS |
no limit | The most layouts on the install, 1 to 100000 (the demo layout counts). Creating one more answers 409 limit_reached |
SLOTTING_MAX_WAVES |
no limit | The most waves stored, on all layouts together, 1 to 1000000. Closed waves count until they are deleted (DELETE /api/waves/{id}, SLOTTING_WAVE_RETENTION_HOURS or SLOTTING_WAVE_MAX_AGE_HOURS); with this cap set, every layout's closed waves past the retention are deleted before a wave create counts |
SLOTTING_MAX_ORDERS_PER_WAVE |
5000 | The most orders in one wave or one POST /api/plan, 1 to 5000 (it can only lower the API's limit) |
SLOTTING_WAVE_MAX_AGE_HOURS |
never | Hours, above 0: when a wave or a layout is created, every wave created longer ago (active, complete or closed, on any layout) is closed and deleted |
SLOTTING_LAYOUT_RETENTION_HOURS |
never | Hours, above 0: when a layout is created, other layouts that haven't changed for longer are deleted, except the demo layout and layouts with a wave that is not closed. A layout that can't be deleted at that moment (busy) is logged and tried again at the next create; the create itself goes on |
SLOTTING_DEMO_SEED |
0 |
1: on start, create the demo layout DEMO if it is missing (never changed when it exists): a fixed wall A of 6 x 8 bins (400 x 300 mm openings, 450 mm deep) and a mobile wall B of 6 x 10 bins (250 x 200 mm, 400 mm deep) standing 5.2 m to the right of A. Bin ids and light addresses are like A-01-01 (wall, row from the top, column). The demo layout is protected: replacing or deleting it, or moving its walls, answers 409 demo_protected, and if it goes missing (a flushed store) it is created again at the next layout list or create |
The caps are checked before the change is written, so two requests at the same moment can both pass a cap; they are meant for a demo, not as quotas. Use them with the memory store, which a restart resets, and with an evaluation license.
The store#
Memory (trial only)#
Without Redis settings the install keeps everything in memory and logs a warning at startup. Layouts, waves and scans are lost on every restart, and the state is not shared with other instances. Use it to evaluate, never in production.
Redis#
Set REDIS_URL (or REDIS_CLUSTER_NODES, or REDIS_SENTINELS with REDIS_SENTINEL_NAME). Any Redis 6.2 or newer works,
including managed services (Amazon ElastiCache or MemoryDB, Azure Cache for Redis, Google Memorystore) and Redis-compatible
servers such as Valkey. Use rediss:// URLs when the server has TLS; with a private CA, add REDIS_TLS_CA_FILE.
- Cluster mode works: all keys of one layout share a hash tag, so they live on one slot and every change to a layout is one atomic transaction.
- If Redis goes down, the API answers
503 store_unavailablewith aRetry-Afterheader,/healthzreportsdegraded, and the install reconnects by itself. Scan stations show "retry"; nothing is lost that was already confirmed. - One install per prefix. Separate installs (test and production, two sites) can share one Redis with different
REDIS_KEY_PREFIXvalues. Instances of the same install use the same prefix (next section).
Persistence and backups#
The plans and the scan state are operational data: if Redis loses them, the wall's contents are no longer known to the system. Configure Redis so it keeps them:
- Enable AOF persistence:
appendonly yeswithappendfsync everysec(at most about one second of scans can be lost on a crash). With webhooks this matters twice: the events not yet delivered (the outbox) live in Redis too, so a Redis that restarts without persistence also forgets the webhooks it still owed. On managed services, choose the option with persistence or durability (for example MemoryDB, or a Redis persistence setting on your cache), and replication with automatic failover. - Never evict: set
maxmemory-policy noeviction. An eviction policy (allkeys-lruand similar) would silently delete waves under memory pressure. Size the memory for your layouts and the waves kept forSLOTTING_WAVE_RETENTION_HOURS(checkINFO memoryafter a few days of normal work). - Backups: keep RDB snapshots (
savesettings, orBGSAVEfrom a scheduled job) and copy them off the server, as for any database. A restore brings back the layouts and the waves as they were at the snapshot; units put since then are not in it, so after a restore, close the waves that were running and start them again. - Don't share the database with applications that call
FLUSHDB, and don't edit theslotting:keys by hand. The webhook keys areslotting:outbox(layouts with undelivered events),slotting:{<layout>}:outbox(the events, in order),slotting:{<layout>}:outbox:leaseandslotting:deadletters(the last 1000 events given up). WithoutSLOTTING_WEBHOOK_URLSno events are kept. With webhooks, a deleted layout's outbox stays until its events are delivered, and each layout keeps at mostSLOTTING_OUTBOX_MAX_PER_LAYOUT(default 100 000) waiting events: over it the oldest are dropped with an error in the log and one dead letter (Outbox limit). Size Redis for that worst case if a receiver can be down for long, or lower the limit.
Running several instances#
For availability or load, run two or more instances against the same Redis with the same REDIS_KEY_PREFIX, behind
your (private) load balancer. They share every layout and wave safely: each change to a layout takes a short lock in Redis
and is written in one transaction, and each instance reloads a layout when another instance changed it. No sticky
sessions are needed, and any instance can answer any scan.
- Give each instance its own
SLOTTING_INSTANCE_ID(slotting-1,slotting-2), and stay within the number of installations in your license. - Changes to one layout are serialized; scans on different layouts run in parallel. If many stations scan into the same
layout at once and requests answer
503 store_busy, raiseREDIS_LOCK_WAIT_MSa little; the scan clients retry a 503 anyway. - Use the memory store with one instance only.
Network and security#
The API has no authentication, by design: anyone who can reach the port can create and delete layouts, close waves and scan. Run the install on a private network only:
- Let only your backend, the scan stations' devices and the people who manage the walls reach it (security groups, firewall rules, a private subnet or VPN). Never give it a public IP address or a public load balancer.
- If your network needs authentication or TLS, put a reverse proxy or API gateway in front of it (it speaks plain HTTP).
The components send their requests to
api-base, so point that at the proxy. - Keep Redis private too, with a password (or ACL user) and TLS where your network requires it.
Because the API has no login, the install also protects itself against web pages that a warehouse PC happens to open:
- Cross-origin changes are refused. A
POST,PUT,PATCHorDELETEunder/api/that carries anOriginheader (browsers send one) answers403 origin_not_allowedunless the origin is the install itself or is listed inSLOTTING_CORS_ORIGINS. Requests withoutOrigin(your backend, curl, scripts) are not affected. SetSLOTTING_CORS_ORIGINSonly for the origins that serve your station pages. - JSON only. Request bodies must be
Content-Type: application/json; anything else (includingtext/plain, which a page could send without asking) answers415 unsupported_media_type. - Host allowlist. With
SLOTTING_ALLOWED_HOSTSset, the install answers only to those host names (421 host_not_allowedotherwise), so a hostile DNS name pointed at its address (DNS rebinding) gets nowhere. Set it in production. - Behind a reverse proxy, forward the original
Hostheader (nginx:proxy_set_header Host $host;) or setX-Forwarded-Host, so the install recognises requests from its own pages; list the proxy's public host name inSLOTTING_ALLOWED_HOSTS.
Logs#
The install logs JSON lines to standard output: one line at startup with the store and the license, one when the license status changes, warnings (memory store, Redis connection lost and restored, a light gateway or MQTT broker that can't be reached, a webhook delivery that failed and will be retried) and errors (a webhook event given up as a dead letter). It never logs license keys, Redis passwords, AI keys, the webhook secret, light tokens or MQTT passwords, or photos.