← 1. The problem · Contents · Next → 3. Architecture
2. Getting started¶
Ten minutes from a clean checkout to a door opening on a badge, with nothing to configure.
flowchart LR
A["make up<br/>starts the stack"] --> B["make edge<br/>starts the cabinet"]
B --> C["make badge<br/>presents a credential"]
C --> D["the door opens"] What you need¶
- Docker with Compose v2 (
docker compose, notdocker-compose) - make
- nc (netcat) and curl for the command-line examples
- Optionally Rust and pnpm, only to run the test suites outside Docker
Nothing else. No database to install, no broker to configure, no certificates to generate. The stack does all of it on first start.
This chapter is about that local stack, the one every other chapter's examples assume. It is not how a real site runs: for that, see 14. Deploying to production.
Start the stack¶
make up
That brings up PostgreSQL, EMQX, Keycloak and MinIO, applies the migrations, creates the log partitions, provisions the object storage buckets, and loads a full development dataset. It is idempotent. Run it as often as you like.
When it finishes it prints the local endpoints:
| Service | URL | Credentials |
|---|---|---|
| Keycloak | http://localhost:8080 | admin / admin |
| EMQX dashboard | http://localhost:18083 | admin / wardn_dev_dashboard |
| MinIO console | http://localhost:9001 | wardn / wardn_dev_secret |
| PostgreSQL | postgres://wardn_owner@localhost:5432/wardn | password wardn_owner_dev |
| MQTT | mqtt://localhost:1883 | anonymous (plaintext listener) |
Keycloak takes around thirty seconds to import its realm on a first start.
A port is already taken?
Bind for 0.0.0.0:8080 failedmeans something else owns it. Every published port is a variable in.env:KEYCLOAK_PORT,POSTGRES_EXPOSED_PORT,MQTT_PORT,EMQX_DASHBOARD_PORT,MINIO_PORT,MINIO_CONSOLE_PORT,BACKEND_PORT,FRONTEND_PORT,EDGE_READER_PORT,EDGE_METRICS_PORT. Change it and runmake upagain. Moving the dashboard's port also moves the origin the browser calls from, so setCORS_ORIGINSandWARDN_API_URLto match.
Check the socle is sound:
make verify # asserts the schema, the grants and the seed
Start the cabinet¶
make edge
This builds the Rust controller and starts it. It comes up, opens its encrypted local database, loads the wiring from edge/bootstrap/edge-01.json, and starts listening for reader frames.
starting wardn edge device_id=edge-01 encrypted=true
provisioned from bootstrap file
door ready name="North entry" direction=In mode=Normal
controller ready credentials=… pending_events=0 relay=mock
reader bus listening address=0.0.0.0:9000
Open a door¶
The local stack has no RS-485 line, so readers are simulated: a reader sends one text frame per line over TCP, in the form <bus address>:<credentialType>:<value>.
make badge VALUE=BDG-0001
which is exactly:
printf '1:badgeNumber:BDG-0001\n' | nc localhost 9100
The controller answers in its log:
▲ door opened door="North entry" pulse_ms=2000
decision door="North entry" granted=true reason=Success elapsed_us=…
Try a plate on the camera at address 2, and a QR code at address 4:
make badge VALUE=1-ABC-123 ADDR=2 TYPE=licensePlate
make badge VALUE=QR-VISIT-0001 ADDR=4 TYPE=qrCode
Walk every decision branch¶
make demo
This presents nine credentials and asserts the reason the controller reports for each. It is the same script CI runs.
| Scenario | Expectation |
|---|---|
| A known badge | Success |
| An unknown badge | UnknownCredential, and nobody named |
| A plate shared by a granted driver and a denied passenger | Success. One grant is enough |
The same plate however the camera spells it (1abc123) | Success. Values are normalised |
| A reissued badge | Success |
| The badge it replaced | UnknownCredential. It is gone from the cabinet |
| A lapsed right | ExpiredRight, not UnknownCredential |
An unknown QR at a door in unlocked mode | UnlockedMode, and still logged |
| A visitor QR at the same door | UnlockedMode |
The rules behind those answers are chapter 4.
Prove the offline mode¶
This is the claim the whole design rests on, so it is worth seeing directly.
make outage
The script freezes the broker. This is what a lost uplink looks like from the cabinet, because the TCP connections and the sessions stay intact. The script presents badges while the broker is down. Then it unfreezes the broker and checks that every passage arrives, each one flagged as replayed and carrying the instant it actually happened.
You can also do it by hand: stop PostgreSQL, EMQX, Keycloak and MinIO, present a badge, and the door still opens. That is not a fallback mode. It is the normal mode.
Start the backend and the dashboard¶
docker compose up -d backend
make dashboard
open http://localhost:4200 # sign in as operator / operator
make badge VALUE=BDG-0001 # watch the row appear while you look at it
The realm wardn ships three accounts:
| Account | Realm roles | What it is for |
|---|---|---|
admin / admin | wardn-admin, wardn-user | The only one allowed to push firmware |
operator / operator | wardn-user | The ordinary operator |
readonly / readonly | none | Checking what a session with no role looks like |
Two development API keys are printed by the seed:
Acme Corp wapi_dev_acme_0000000000000001
Northwind wapi_dev_northwind_000000000002
The development dataset¶
The seed is not filler. It is built around the cases that are easy to get wrong, so they are visible from the first minute instead of surfacing in production:
- a licence plate shared by two people: one right grants, the other denies. The cabinet must open, because at least one right grants it.
- an expired right that is still an active row, with badge reads after it lapsed.
- a replaced right: Tom lost his badge, so his right was superseded rather than edited. The chain is visible through
replacesId. - a user with no name, created from a QR code alone.
- unknown credentials, logged with a raw value and no user to join to.
- a door left unlocked that still logs every read, and a locked one.
- an offline batch replayed after a two-hour outage.
- three zones in three timezones, so a UTC instant renders differently per site.
Two tenants exist: Acme Corp and Northwind. They exist precisely so that confinement between customers can be exercised rather than just asserted.
The rest of the demonstrations¶
Each of these is a script that asserts its claims and exits non-zero when one fails. They are what CI runs, and they are the fastest way to see a feature work.
| Command | What it proves |
|---|---|
make demo | Every decision branch of the cabinet |
make outage | An uplink outage loses nothing |
make sync | A right changed through the API reaches the door |
make api | Authentication, tenant confinement, immutability, audit |
make live | A passage reaches an open dashboard, and only its tenant's |
make fleet | Telemetry, remote log level, live debug |
make commands | A door opened, locked and a service restarted from the cloud |
make ota | A firmware installed, a corrupt one refused, a broken one rolled back |
make compliance | Tracing, personal-data export, erasure, retention |
make scale | Two backends, one event, handled once |
make mtls | Both ends prove who they are, strangers are turned away |
make load | A burst of passages inside the decision budget |
make backup / make backup-verify | A backup is taken, and it restores |
make tls-check | How long every certificate in the stack has left |
Day-to-day commands¶
make help # every target, with a one-line description
make ps # what is running
make logs # follow everything
make edge-logs # follow the cabinet only
make psql # a database shell as the owner role
make down # stop, keep the data
make reset # destroy the volumes and rebuild from scratch
make clean # containers, volumes and orphans
Useful knobs while developing:
SEED_ACTIVITY_DAYS=90 SEED_ACTIVITY_EVENTS_PER_DAY=40 make reset # a larger history
SEED_ENABLED=false make reset # an empty database
make reseed # re-run the seed in place
make edge-reset # wipe the cabinet's database
It runs. Now let us look at what is actually running.
Next → 3. Architecture