Stand Up the Web Client¶
What this is: deploying the browser client — the alternative to the Android app for getting onto the WayPoint network. See System overview. When you'd do it: bringing the rack up, or bringing the web tier back after it was switched off. How long it takes: minutes — it is part of a normal converge. The only slow part is minting the service identity token by hand.
The web client is an AdonisJS application that serves a real-time tactical map and handles user login. It needs two things to do its job: a Directory (to authenticate users and issue identity tokens) and a server (a Zenoh relay to carry traffic — the browser reaches it through a remote-api bridge, see Step 3 → Server / relay).
On the PETRA rack it runs as a container on core-01, alongside the Directory, its
Postgres, and the Caddy that fronts both. You do not provision it separately: it is a role
in playbooks/core.yml, and a converge stands it up.
Who can do this: a deployment engineer with rack access — an SSH key in
ssh_authorized_keysand the vault password on their laptop (see theinfrastructurerepo's Onboarding section). Minting the service token additionally needs a Directory Admin login. Users still need onboarding to the Directory before they can log in.
The shape of it¶
flowchart LR
A["ansible-playbook<br/>playbooks/core.yml"] --> B["web + zenoh-bridge<br/>containers on core-01"]
B --> C["Caddy fronts<br/>app.bedrockdefence.com"]
C --> D["Directory handles<br/>passkey login;<br/>issues session"]
D --> E["User sees the<br/>live tactical map"]
Before you start¶
- A running Directory on
core-01(https://auth.bedrockdefence.com). It comes up earlier in the same play —playbooks/core.ymlorders Postgres → Directory → web. - At least one running server (relay) on
core-02— users see no map data without one. - Your controller is set up:
bin/bootstrap-controller.shhas put the vault password on your laptop and your SSH key is on the boxes. - The web image is in the rack registry.
playbooks/foundation.ymlmirrors it there viabin/mirror-to-rack.sh; the rack pulls from itself and never from Artifact Registry.
Step 1 — Converge¶
That writes web's on-disk state and its Compose fragment, and
roles/platform/compose brings the whole project up. There is nothing web-specific to run.
Three secrets are generated on the first converge and then left alone, because
roles/service/web writes its secrets file with force: false:
| Secret | Where it comes from |
|---|---|
APP_KEY |
generated in playbooks/core.yml |
WAYPOINT_API_JWT_SECRET |
generated in playbooks/core.yml |
DIRECTORY_WEBAUTH_CLIENT_SECRET |
read out of the Directory's own persisted secrets file, so both sides match |
Because of force: false, changing those inputs later does not change a running
deployment — delete /etc/bedrock/web/secrets.env on core-01 to pick up new values.
Step 2 — Give web its machine identity¶
The web tier polls the Directory's /api/revoked-principals endpoint and signs the
envelopes it publishes. Both need a Directory-signed IdentityToken. There is no
node ace command for it — mint it from the Directory admin UI:
- Sign in to the Directory as an Admin, click Devices → Create Device.
- Give it a callsign (e.g. web-rack) and set Platform Type to web — the web tier holds a Directory-signed IdentityToken but is a client, not a router, so it needs no hostname and no coverage cells.
- On the Device Created page, download both credentials: the Identity Token and the Signing Key. They are shown once. If you navigate away without saving them, use Reissue Credentials to mint a fresh pair.
Then put both into the vault — this is the delivery mechanism, there is no upload script:
base64 -i web-rack-service-token.bin | tr -d '\n' # → web_service_token_b64
ansible-vault edit inventories/rack/group_vars/all/vault.yml
| Vault key | Value |
|---|---|
web_service_token_b64 |
the identity token .bin, base64-encoded |
web_service_signing_key_hex |
the signing key as 64 hex characters, pasted verbatim (no base64) |
Re-run playbooks/core.yml. Ansible decodes the token on the controller and copies the
bytes across, so an unchanged token is a genuine no-op rather than a permanent "changed".
Both values are fail-soft, and they fail differently:
- Without the identity token, the revocation cache never primes and logins fail closed. You will notice immediately.
- Without the signing key, the web tier cannot sign what it publishes, so server-side control-measure and plan-shape distribution stays switched off. The app starts and looks healthy. There is no error to notice, so don't skip it.
Supply them together. The signing key's public half is embedded in the identity token presented for verification, so a token paired with a stale key from a previous mint is rejected — reissuing means replacing both.
Step 3 — What is already wired for you¶
Most of what used to be manual configuration is derived on the rack. This section is mainly so you know where to look when something is wrong.
Directory (for login)¶
Web is pointed at https://auth.bedrockdefence.com and shares
DIRECTORY_WEBAUTH_CLIENT_SECRET with the co-located Directory, so there is no client
registration step. Web reaches it over HTTPS behind a rack-CA leaf that no public trust
store knows, which is why the container gets NODE_EXTRA_CA_CERTS pointing at the rack CA.
If that CA is missing, web starts and then fails every call to the Directory — logins
included.
The callback URI is webauth_redirect_uris in group_vars/all/vars.yml.
Server / relay (for map traffic)¶
Browsers join the Zenoh mesh through a remote-api bridge, not directly. The browser
library (@eclipse-zenoh/zenoh-ts) speaks only the zenoh-plugin-remote-api WebSocket
protocol — not the router's native tls/quic transport. That is why Android connects
on quic/…:7447 but a browser pointed at the same port just gets a 1006 and
reconnect-loops.
On the rack the bridge is a sidecar container on core-01, and Caddy publishes its
WebSocket as wss on app.bedrockdefence.com:7448:
| Piece | Value | Set where |
|---|---|---|
| Public live bridge dials the mesh | ordered native locators for every configured router | web_bridge_connect_endpoints |
| Browser endpoint | app.bedrockdefence.com — the app expands a bare domain to wss://<domain>:7448 |
web_router_endpoints |
| Web retained-query endpoints | one Docker-network-only remote-api bridge pinned to each configured router | web_query_bridge_connect_endpoint_groups |
| Router trust | rack CA, server-authenticated TLS | web_bridge_ca_b64 |
The public bridge carries browser live traffic. The Web backend separately sends each authenticated retained query through every private query bridge because retained stores are router-local and are not replicated by federation. Each returned page still needs its valid signed Server receipt before Web accepts it.
Every bridge has the same transport posture as Android and every other node. It presents no special certificate identity and receives no fleet-wide ACL grant; Web declares only the exact selectors in its current Directory-issued authority. mTLS remains available only as a deployment-wide option when every node supports it.
The bridge version must match the router's zenoh version and the web client's
@eclipse-zenoh/zenoh-ts — bump all three together.
Machine ingest and external imagery¶
| Config key | Required? | What it does |
|---|---|---|
WAYPOINT_API_JWT_SECRET |
Yes | HS256 secret signing the machine ingest JWTs for the force-tracking /api/feed/* API. Must be at least 32 characters in production or the app fails at boot. Rotating it invalidates all outstanding ingest tokens at once, with no overlap window. To mint a credential see Issue an API credential; the contract is in Machine Ingest API. |
WAYPOINT_IMAGERY_IMG_SRC |
For remote imagery | Explicit origin allowlist feeding the CSP img-src directive for external GEOINT/CAT-1 imagery loaded by URL. Every trusted imagery origin must be listed: production has no blanket https: source. On the rack, web_imagery_img_src sets it to https://imagery.bedrockdefence.com. An omitted origin is blocked, reported to /api/csp-report, and shown as an imagery-unavailable placeholder; an invalid configured URL fails the app at boot. |
WAYPOINT_API_JWT_SECRET is the signing root for a separate, machine-to-machine trust
boundary — it is not Directory identity and is not classification-gated.
Step 4 — Users sign in¶
Users browse to https://app.bedrockdefence.com. The web client redirects to the Directory's
browser auth flow, the user authenticates with their passkey, and the Directory issues a
session back.
Browsers will warn about the certificate until the rack CA is trusted on the client
machine — see environments/rack/README.md in the infrastructure repo. This is expected:
the rack mints its own certificates and has no public CA.
Users must already be onboarded in the Directory. If not, see Onboard an operator.
How to know it worked¶
https://app.bedrockdefence.comloads (no certificate warning once the rack CA is trusted).- A user can click Log in, authenticate with their passkey, and land on the map.
- The map shows live unit positions for the area the connected server covers.
- On
core-01,docker compose -f /etc/bedrock/compose/docker-compose.yml psshows bothwebandzenoh-bridgeup.
If something goes wrong¶
-
Every call to the Directory fails; logins included. The rack CA never reached the container, so
NODE_EXTRA_CA_CERTSpoints at a file that does not exist — Node only warns about that, so the app starts and then fails every HTTPS call. The converge prints a message when this happens; re-runplaybooks/core.yml. -
Logins fail closed. The service identity token is missing, so the revocation cache never primes. Check
web_service_token_b64in the vault and re-run. -
Everything looks healthy but control measures never distribute. The signing key is missing. This is the fail-soft one with no error — check
web_service_signing_key_hexin the vault. -
Can't log in — "user not found" or access denied. The user isn't onboarded to the Directory yet. See Onboard an operator.
-
Site loads but the map is empty. Check, in order: the browser console — a
1006/ "disconnected from remote-api-plugin" loop means the browser is pointed at the router's:7447rather than the bridge'swss://…:7448; then that thezenoh-bridgecontainer is up and its endpoints reach a live router; then that a server is live and covers the relevant geohash cells. See Add a server. -
Certificate warning in the browser. The rack CA isn't trusted on that machine. This is not a misconfiguration — see
environments/rack/README.md. -
app.bedrockdefence.comdoesn't resolve. This is the owned, browser-facing name. Rack services resolve it through the managed/etc/hostsblock; an attached operator laptop needs the reviewed LAN name-to-address path, while an off-LAN managed device needs active WARP and the Cloudflare private-hostname route. Do not substitute a.lanservice name — that suffix is retained only for rack-internal management names. -
Reaching it from off the LAN.
443and7448are opened only torack_lan_cidr. Off-site access is through the Cloudflare tunnel, not by widening the firewall.
Local development¶
The web repo runs standalone for development — .env.example is the starting point and
npm run dev runs migrations and serves with HMR. It needs a reachable PostgreSQL, which
you supply yourself. See that repo's README.md; it is the source of truth for the
environment variables the app validates at startup (start/env.ts).
See also¶
- Operator training index
- Set up the Directory — comes up before web in the same play
- Add a server — must be live for map data to flow
- Onboard an operator — users need Directory accounts first
- Issue an API credential — mint a machine ingest token
- Machine Ingest API — the
/api/feed/*contract - The
infrastructurerepo —roles/service/web/andplaybooks/core.yml - The
webrepo —README.mdandstart/env.tsfor the environment variable reference
Verified against web@f5683354 / infrastructure@5ac55aeb on 2026-09-02 — Directory/Web browser origins, callback and bridge publication were checked against inventories/rack/group_vars/all/vars.yml, roles/service/web/, and playbooks/core.yml. CSP behavior was checked against Web's config/shield.ts, start/env.ts, and CSP tests; the rack supplies the exact https://imagery.bedrockdefence.com origin through web_imagery_img_src.