Skip to content

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: standing up a new deployment, or adding a browser entry-point to an existing one. How long it takes: an hour for the cloud path if your Directory and a server are already live; a few minutes for the local/Docker path.

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 the web VM runs; see Step 2 → Server / relay). Stand those up first — this runbook is Step 4 in the deployment order; follow Stand up a deployment for the right sequence.

Who can do this: a deployment engineer with access to the GCP project (for the cloud path) or a Docker host. The web client has no in-Directory registration step — you do not need an Admin for the provisioning itself, but users still need to be onboarded to the Directory before they can log in.

The shape of it

flowchart LR
    A["Engineer provisions<br/>the web VM<br/>(or Docker host)"] --> B["Point it at the<br/>Directory (OIDC)<br/>and a server (WSS)"]
    B --> C["Users browse<br/>to the site"]
    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 — the web client uses it for OIDC login. The demo Directory is at https://auth.bedrockdefence.com.
  • At least one running server (relay) — users won't see map data without one. Note its native locator (e.g. tls/demo.bedrockdefence.com:7447); the web VM's remote-api bridge dials it. Browsers do not connect to the router directly (see Step 2 → Server / relay).
  • For the cloud path: tofu (OpenTofu) and gcloud installed and authenticated.
  • For the Docker path: Docker installed and a PostgreSQL instance reachable.

Step 1 — Provision the web client

There are two paths. Use the cloud path for real deployments.

Production web environments are a COS VM running Caddy + AdonisJS + Cloud SQL Auth Proxy, backed by a Cloud SQL PG17 instance. This lives in the infrastructure repo. For a new environment, copy terraform/environments/web-demo/ to a new directory and adjust variables.tf. For the existing demo environment, apply from the existing stack:

cd terraform/environments/web-demo
tofu init && tofu apply

tofu apply creates the VM, a static IP, and a Cloud SQL PG17 instance. Web secrets (app-key, db-password, cloudsql-postgres) are auto-generated by Terraform and stored in Secret Manager — you don't mint them manually.

After the first apply, the web app also needs a service identity token so it can poll the Directory's /api/revoked-principals endpoint. This is a Directory-signed IdentityToken, issued the same way as any service device — there is no node ace command for it; you mint it from the Directory admin UI:

  1. Sign in to the Directory as an Admin, click DevicesCreate Device.
  2. Give it a callsign (e.g. web-demo) and set Platform Type to web — the web tier is a service platform that holds a Directory-signed IdentityToken (for the revocation / group-key polls) but is a client, not a router, so it needs no hostname and no coverage cells.
  3. On the Device Created page, download both credentials as .bin files: the Identity Token and the Signing Key. They are shown once and cannot be recovered — if you navigate away without saving them, use Reissue Credentials to mint a fresh pair.

Then upload both, from the infrastructure repo:

./infra/gcp/secrets/upload-web-service-token.sh ~/Downloads/web-demo-service-token.bin demo
./infra/gcp/secrets/upload-web-signing-key.sh   ~/Downloads/web-demo-signing.key      demo

The VM fetches both from Secret Manager on every boot.

  • Without the identity token, the revocation cache fails to prime and logins fail closed.
  • Without the signing key, the web tier cannot sign the envelopes it publishes, so server-side control-measure and plan-shape distribution stays switched off. This one fails soft — the app starts and looks healthy, it just never distributes. There is no error to notice, so don't skip it.

Upload the two 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 batch is rejected — reissuing means re-uploading both.

DNS must resolve before Caddy can provision its TLS certificate:

app.bedrockdefence.com  →  static_ip from web-demo stack (tofu output)

Option B — Docker

For a local or lab environment, the web repo contains a docker-compose.yml that starts a PostgreSQL instance:

docker compose up -d

Then copy and configure the environment file:

cp .env.example .env
node ace generate:key   # paste the output into APP_KEY in .env

To run the app in a single container instead:

docker build -t waypoint-web .

docker run -d \
  --name waypoint-web \
  -p 3333:3333 \
  -e NODE_ENV=production \
  -e APP_KEY=<your-app-key> \
  -e DB_HOST=<db-host> \
  -e DB_PASSWORD=<db-password> \
  waypoint-web

For development, after docker compose up -d to start the database:

npm install
npm run dev

The app starts at http://localhost:3333. Migrations run automatically on dev startup.

Step 2 — Point the web client at your Directory and server

Directory (for login)

The web client authenticates users via OIDC against your Directory. Set the Terraform variable (cloud path) or environment variable (Docker path):

Config key What it does Example
directory_url (Terraform) / WAYPOINT_OIDC_ISSUER_URL (env) Directory OIDC issuer URL https://auth.bedrockdefence.com
WAYPOINT_OIDC_CLIENT_ID OIDC client ID registered in the Directory
WAYPOINT_OIDC_CLIENT_SECRET OIDC client secret

On the cloud path, the OIDC client secret is pulled from Secret Manager automatically (bedrock-<env>-webauth-client-secret). No manual realm/client setup is required — the auth stack provisions it and the web module consumes it.

To disable password login and rely on passkeys/OIDC only:

Variable Default Effect
WAYPOINT_DISABLE_PASSWORD_AUTH false Set true to hide the password login form
WAYPOINT_ENABLE_WEBAUTHN false Set true to enable passkey (FIDO2/WebAuthn) login

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's why Android connects on quic/…:7447 but a browser pointed at the same port just gets a 1006 and reconnect-loops). The cloud web environment runs the standalone zenoh-bridge-remote-api sidecar on the web VM: it joins the mesh as a client over native tls/7447, and Caddy fronts its WebSocket as wss on :7448. Set:

The bridge is standard — you don't turn it on, you just tell it which router(s) to dial. Everything else derives:

Config key Required? What it does
zenoh_bridge_connect_endpoints (Terraform) Yes Native locators the bridge dials into the mesh, e.g. tls/demo.bedrockdefence.com:7447 (list several for failover). The one input you must set.
zenoh_router_endpoints (Terraform) No Override for the browser endpoint; empty derives wss://<hostname>:7448. Passed to the app as WAYPOINT_ROUTER_ENDPOINTS.
zenoh_bridge_image (Terraform) No Override; empty derives the pinned image in this project's Artifact Registry.
zenoh_bridge_tls_secret_prefix (Terraform) No Override; empty derives bedrock-<resource>-zenoh-bridge-tls.

Two out-of-band prerequisites (same class as the web's own TLS cert and service token):

  1. Image — run terraform/modules/web-environment/zenoh-bridge/build-push.sh <project> <region> once (re-run only on a version bump). The bridge version must match the router's zenoh version and the web client (@eclipse-zenoh/zenoh-ts) — bump all three together.
  2. mTLS cert — Terraform creates the empty …-zenoh-bridge-tls-{cert,key,ca}-pem secret containers; mint the material with pki/generate.sh --add node and upload it.

Until the cert is uploaded the bridge skips itself and the web app stays up (realtime degraded); a reboot after the cert lands brings it online — so tofu apply is safe before the cert exists.

On the Docker path, run the bridge yourself (or point at an existing one) and set WAYPOINT_ROUTER_ENDPOINTS to its WSS address — check web/start/env.ts for the exact var name the app validates at startup.

Machine ingest and external imagery

Two more environment variables govern the web tier's machine-facing edges — the force-tracking ingest API and external imagery loaded by URL. Set them on the same tier as the rest of the web config (Terraform variable on the cloud path, .env on the Docker path):

Config key Required? What it does
WAYPOINT_API_JWT_SECRET Yes HS256 secret that signs and verifies the machine ingest JWTs for the force-tracking /api/feed/* API. A web deploy with this unset fails at boot — the app fails closed. Rotating it invalidates all outstanding ingest tokens at once (no overlap window). To mint a credential against this secret see Issue an API credential; the API contract is in Machine Ingest API.
WAYPOINT_IMAGERY_IMG_SRC No Space/comma-separated origin allowlist feeding the CSP img-src directive, for external GEOINT/CAT-1 imagery loaded by URL from an object store (S3/MinIO/NiFi). Needed when the imagery origin is non-HTTPS or on a non-standard port (e.g. a self-hosted MinIO on http:) — HTTPS hosts are already covered by the blanket https: source. If unset or misconfigured, GEOINT <img> is silently CSP-blocked.

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. See Machine Ingest API for how the credential, scopes, and revocation registry fit together.

Step 3 — Users sign in

Once the web client is up and pointed at the Directory, users browse to the site (e.g. https://app.bedrockdefence.com). The login flow is handled by the Directory: the web client redirects to the Directory's OIDC endpoint, the user authenticates with their passkey, and the Directory issues a session back to the web client.

Users must already be onboarded in the Directory before they can log in. If a user isn't provisioned yet, see Onboard an operator.

How to know it worked

  • The site loads at the configured hostname without a certificate warning.
  • A user can click Log in, authenticate with their passkey on the Directory, and land on the tactical map.
  • The map shows live unit positions for the area covered by the connected server.
  • Other users connected to the same server appear on the map.

If something goes wrong

  • Can't log in at all — OIDC error or redirect loop. The WAYPOINT_OIDC_ISSUER_URL / directory_url is wrong, or the OIDC client secret hasn't been uploaded. Check that the Directory is reachable and that the bedrock-<env>-webauth-client-secret Secret Manager value exists. Also check that the web service identity token has been uploaded — without it, logins fail closed (the revocation cache can't prime).

  • Can't log in — "user not found" or access denied. The user hasn't been onboarded to the Directory yet. See Onboard an operator.

  • Site loads but the map is empty / no other users appear. Either the remote-api bridge isn't reaching the mesh, or no server covers the users' area. Check, in order: the browser console — a 1006 / "disconnected from remote-api-plugin" loop means zenoh_router_endpoints is empty or pointing somewhere without a bridge (it must be the bridge's wss://…:7448, not the router's :7447); then that the bridge container is up and its zenoh_bridge_connect_endpoints point at a live router; then that a server is live and covers the relevant geohash cells. See Step 2 → Server / relay and Add a server.

  • Site won't load — certificate error. DNS hasn't propagated yet, or Caddy couldn't provision a Let's Encrypt certificate. DNS must resolve to the static IP before Caddy can issue the cert. Check with dig app.bedrockdefence.com and compare against tofu output static_ip in the web-demo stack.

  • Database not reachable. On the cloud path, the Cloud SQL Auth Proxy sidecar handles the connection — check that the VM's service account has the roles/cloudsql.client role and that the Cloud SQL instance is running. Tail the serial console:

    gcloud compute instances get-serial-port-output bedrock-web-demo \
      --zone=europe-west2-a | tail -200
    

See also


Verified against directory@61a13c9 / web@80e3ec2 on 2026-06-14 — provisioning: infrastructure repo terraform/environments/web-demo/; client config: web repo README.md. The web service token is a Directory-signed IdentityToken minted from the admin Devices flow as a web-platform device (devices_service.ts, device_credentials.edge) — web holds an IdentityToken and a paired device signing key, but no hostname/coverage/ServerToken; there is no directory:create-service-token ace command. The signing key is loaded from WAYPOINT_SERVICE_SIGNING_KEY_PATH (web start/env.ts) and rotated in lockstep with the token (app/domains/auth/service_token_manager.ts); absent, plan_shape_distributor.ts gets null credentials and distribution is disabled fail-soft. Production containers have no node ace access (directory start/routes.ts, keys_controller.ts); node ace generate:key here is the dev/lab APP_KEY bootstrap only (web/directory README.md) — production app keys are auto-generated by Terraform.