Skip to content

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, not docker-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 failed means 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 run make up again. Moving the dashboard's port also moves the origin the browser calls from, so set CORS_ORIGINS and WARDN_API_URL to 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