STATEBOARD/docs

Self-hosting

Docker Compose, Helm, Keycloak setup, airgap-friendly deployment.

StateBoard v1 is designed to deploy inside a company's own perimeter. One container + Postgres + your existing IdP. No outbound calls except to your IdP at runtime.

Local dev (Docker Compose)

The repo ships a docker-compose.yaml with Postgres + a pre-seeded Keycloak realm. Two test users (alice@stateboard.dev, bob@stateboard.dev) with passwords matching their usernames.

cp .env.example .env
docker compose up -d
pnpm install
pnpm migrate
pnpm dev

Open http://localhost:3000, click Continue with Keycloak, sign in as alice. The first sign-in becomes the workspace owner.

Production: Docker

The shipped image only contains the Node app — bring your own Postgres and IdP.

docker run --rm \
  -p 3000:3000 \
  -v stateboard-uploads:/data \
  -e DATABASE_URL=postgres://user:pass@db:5432/stateboard \
  -e BETTER_AUTH_SECRET="$(openssl rand -base64 32)" \
  -e STATEBOARD_BASE_URL=https://stateboard.example.com \
  -e KEYCLOAK_ISSUER=https://keycloak.example.com/realms/acme \
  -e KEYCLOAK_CLIENT_ID=stateboard \
  -e KEYCLOAK_CLIENT_SECRET=... \
  ghcr.io/saschb2b/stateboard:1.0.0

Schema is managed by pnpm migrate (or node scripts/migrate.mjs) — run it once after bringing up Postgres, and on every upgrade.

Production: Helm

helm dependency build deploy/helm/stateboard
helm install stateboard ./deploy/helm/stateboard \
  --namespace stateboard --create-namespace \
  --set auth.baseUrl=https://stateboard.example.com \
  --set auth.secret="$(openssl rand -base64 32)" \
  --set auth.keycloak.issuer=https://keycloak.example.com/realms/acme \
  --set auth.keycloak.clientSecret=$KC_SECRET \
  --set postgresql.auth.password="$(openssl rand -base64 16)"

The chart bundles a Bitnami Postgres sub-chart by default. A pre-install / pre-upgrade Job runs pnpm migrate before any pods come up.

To use an external Postgres:

--set postgresql.enabled=false \
--set externalDatabaseUrl=postgres://user:pass@db.example.com:5432/stateboard

Or read the URL from a Secret via externalDatabaseUrlExistingSecret.{name,key}.

Multi-replica deployments need persistence.accessMode: ReadWriteMany for the uploads volume — Postgres handles concurrent connections fine on its own, but two pods writing screenshots to the same RWO PVC will not.

Keycloak setup

  1. In your realm, create a confidential client with id stateboard (or anything you set in KEYCLOAK_CLIENT_ID).
  2. Set the Valid redirect URI to https://stateboard.example.com/api/auth/callback/keycloak.
  3. Set Web origins to https://stateboard.example.com.
  4. Copy the client secret into KEYCLOAK_CLIENT_SECRET.
  5. Set KEYCLOAK_ISSUER to the realm URL: https://keycloak.example.com/realms/<realm>.

That's it — StateBoard discovers all OIDC endpoints from the issuer's .well-known/openid-configuration.

Email-domain allowlist

By default, anyone Keycloak considers signed-in can join the workspace (subject to whatever the realm enforces). To narrow further:

STATEBOARD_ALLOWED_EMAIL_DOMAINS=acme.com,acme.de

Sign-ins from other domains see a "your email domain isn't allowed" error during the OIDC return.

First user becomes owner

The first user to complete sign-in becomes the workspace owner. Subsequent users land as editor by default — change this with STATEBOARD_DEFAULT_ROLE=viewer if you want new sign-ins to be read-only until promoted.

The owner can change roles or remove members at /settings/members.

Airgap considerations

The runtime makes only one outbound call: to your configured Keycloak issuer (and only during sign-in). No telemetry, no analytics, no license server, no phone-home, no remote font loading. The docs search index is built locally with Orama.

If your IdP is inside the same network as StateBoard (which it almost certainly is), the deployment is airgap-clean.

Backups

Two things to back up:

  1. Postgres — every board, screen, region, member, and audit row.
  2. Uploads PVC (/data) — the screenshot files themselves. Filenames are recorded in the DB, so the two backups must restore in lockstep.

A typical screenshot is 0.5–2 MB. A team running quarterly reviews on 10 products with ~10 screens each will use well under 1 GiB per year.

Upgrading

Helm:

helm dependency build deploy/helm/stateboard
helm upgrade stateboard ./deploy/helm/stateboard \
  --namespace stateboard --reuse-values \
  --set image.tag=NEW_VERSION

The migrate Job runs automatically before the new pods come up. If you disabled it (migrate.enabled=false), run pnpm migrate against the database manually first.

What we'll add later

  • v1.x: headless capture from URL, Jira issue linking.
  • v2: scheduled re-capture, time-travel / diff view, Slack notifications, Notion / Confluence embed.
  • v3: auto region-detection from the DOM, journey views, portfolio rollup, audit-log UI.

On this page