{"id":50111436,"url":"https://github.com/saschb2b/stateboard","last_synced_at":"2026-06-03T15:00:39.832Z","repository":{"id":354873059,"uuid":"1225402721","full_name":"saschb2b/stateboard","owner":"saschb2b","description":"Status reporting for visual products — built around the screens stakeholders actually see, not the tickets engineers actually file","archived":false,"fork":false,"pushed_at":"2026-06-02T20:48:06.000Z","size":679,"stargazers_count":0,"open_issues_count":20,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T21:19:35.851Z","etag":null,"topics":["deployment","product"],"latest_commit_sha":null,"homepage":"https://saschb2b.github.io/stateboard/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/saschb2b.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-30T08:40:14.000Z","updated_at":"2026-06-02T20:48:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/saschb2b/stateboard","commit_stats":null,"previous_names":["saschb2b/stateboard"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/saschb2b/stateboard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saschb2b%2Fstateboard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saschb2b%2Fstateboard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saschb2b%2Fstateboard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saschb2b%2Fstateboard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/saschb2b","download_url":"https://codeload.github.com/saschb2b/stateboard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saschb2b%2Fstateboard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33870026,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deployment","product"],"created_at":"2026-05-23T12:32:53.539Z","updated_at":"2026-06-03T15:00:39.825Z","avatar_url":"https://github.com/saschb2b.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StateBoard\n\n[![Release](https://img.shields.io/github/v/release/saschb2b/stateboard?display_name=tag\u0026sort=semver\u0026label=release\u0026color=ea580c)](https://github.com/saschb2b/stateboard/releases/latest)\n[![CI](https://github.com/saschb2b/stateboard/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/saschb2b/stateboard/actions/workflows/ci.yml)\n[![Docker](https://github.com/saschb2b/stateboard/actions/workflows/docker.yml/badge.svg?branch=main)](https://github.com/saschb2b/stateboard/actions/workflows/docker.yml)\n[![Pages](https://github.com/saschb2b/stateboard/actions/workflows/pages.yml/badge.svg?branch=main)](https://saschb2b.github.io/stateboard/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n\n📦 **Current release:** [`2026.6.0` (team-ready)](https://github.com/saschb2b/stateboard/releases/latest). See the [changelog](./CHANGELOG.md) for what shipped and what's intentionally not here yet.\n\n🟢 **[Live read-only demo →](https://saschb2b.github.io/stateboard/)**. Explore the example board, hover regions, try Present mode. The editor itself needs the self-hosted version (link below).\n\n\u003e Status reporting for visual products, built around the screens stakeholders actually see, not the tickets engineers actually file.\n\n**Show, don't tell.** Upload a screenshot of your app. Drag rectangles over the parts you want to talk about. Tag each one as `SHIPPED`, `MOCK`, or `MISSING`. Share one link. Your exec reads it in 30 seconds.\n\nOpen source. Self-hosted. Airgap-ready (except for your own SSO). MIT.\n\n---\n\n## v1 (team-ready)\n\nThis is `v1`, the cut you can deploy in a company.\n\n- Manual screenshot upload (PNG / JPEG / WebP / GIF, up to 25 MB)\n- Region tagging on the image (click + drag)\n- Three states: `shipped` / `mock` / `missing`\n- Public read-only share links: revocable, multiple per board\n- **Multi-user via Keycloak / OIDC** (any OIDC-compliant IdP works; Keycloak is the documented default)\n- **Roles**: owner / editor / viewer\n- **Append-only audit log** of mutations (read directly from Postgres for now)\n- Postgres-backed, multi-replica safe (with `ReadWriteMany` for uploads)\n- One container + one Postgres. Zero outbound calls except to your IdP.\n\nWhat's not here yet (by design, see [the build plan](#roadmap)): headless capture, Jira sync, scheduled re-capture, diffs, journeys.\n\n## Quick start (local dev)\n\nThe repo ships a `docker-compose.yaml` with Postgres + Keycloak pre-seeded with two test users (`alice` / `bob`, password = same as username).\n\n```bash\ncp .env.example .env\ndocker compose up -d           # starts postgres + keycloak\npnpm install\npnpm migrate                    # creates tables\npnpm dev\n```\n\nOpen \u003chttp://localhost:3000\u003e, click **Continue with Keycloak**, sign in as `alice`. The first sign-in becomes the workspace owner.\n\n## Production: Docker Compose (bring your own Keycloak)\n\nIf you already run Keycloak (or any other OIDC provider) and just want StateBoard + a Postgres next to it, [`deploy/docker-compose.yaml`](./deploy/docker-compose.yaml) is the path. Postgres + StateBoard + a one-shot migrate job, no bundled IdP.\n\n```bash\ncp deploy/docker-compose.env.example deploy/.env\n# edit deploy/.env: set STATEBOARD_BASE_URL, BETTER_AUTH_SECRET,\n# POSTGRES_PASSWORD, and the three KEYCLOAK_* vars\ndocker compose -f deploy/docker-compose.yaml --env-file deploy/.env up -d\n```\n\nIn your existing Keycloak realm, create a confidential client and register `\u003cSTATEBOARD_BASE_URL\u003e/api/auth/oauth2/callback/keycloak` as a Valid redirect URI. See [Self-hosting → Keycloak setup](https://saschb2b.github.io/stateboard/docs/self-hosting#keycloak-setup) for the full checklist. Same env-var names work with any OIDC IdP; only the issuer URL changes.\n\nTerminate TLS at your reverse proxy (Caddy, nginx, Traefik, a cloud LB) so `STATEBOARD_BASE_URL` is HTTPS.\n\n## Production: Helm\n\n```bash\nhelm dependency build deploy/helm/stateboard\nhelm install stateboard ./deploy/helm/stateboard \\\n  --namespace stateboard --create-namespace \\\n  --set auth.baseUrl=https://stateboard.example.com \\\n  --set auth.secret=\"$(openssl rand -base64 32)\" \\\n  --set auth.keycloak.issuer=https://keycloak.example.com/realms/acme \\\n  --set auth.keycloak.clientSecret=... \\\n  --set postgresql.auth.password=\"$(openssl rand -hex 32)\"\n```\n\nThe chart bundles a Bitnami Postgres sub-chart by default. Disable with `--set postgresql.enabled=false` and provide `--set externalDatabaseUrl=...` (or read it from a Secret via `externalDatabaseUrlExistingSecret`). See [`deploy/helm/stateboard/values.yaml`](./deploy/helm/stateboard/values.yaml) for the full reference.\n\nA pre-install / pre-upgrade Job runs `pnpm migrate` before any pods come up. Disable with `--set migrate.enabled=false` if you'd rather run schema changes out-of-band.\n\n## Architecture\n\n| Piece        | Choice                  | Why                                                  |\n| ------------ | ----------------------- | ---------------------------------------------------- |\n| Framework    | Next.js 16 (App Router) | One process serves UI + API + uploads                |\n| UI           | React 19 + MUI 7        | Solid component library, small enough to skin        |\n| Persistence  | Postgres via `pg`       | Multi-replica safe; standard ops your team knows     |\n| Auth         | Better Auth + OIDC      | First-class Keycloak helper; sessions in the same DB |\n| File storage | Local filesystem        | RWX PVC for v1; S3-compatible adapter planned for v2 |\n| Telemetry    | None                    | Airgap by default, that's the point                  |\n\nEverything that touches the DB lives in `src/lib/db.ts`. Auth wiring is in `src/lib/auth.ts` (server) and `src/lib/auth-client.ts` (browser). Schema is plain SQL under `migrations/`, applied by `scripts/migrate.mjs`.\n\n## Scripts\n\n| Command             | What it does                                        |\n| ------------------- | --------------------------------------------------- |\n| `pnpm dev`          | Run on `localhost:3000`                             |\n| `pnpm build`        | Production build                                    |\n| `pnpm start`        | Run the production build                            |\n| `pnpm migrate`      | Apply pending SQL migrations against `DATABASE_URL` |\n| `pnpm lint`         | ESLint                                              |\n| `pnpm typecheck`    | `tsc --noEmit`                                      |\n| `pnpm test`         | Unit tests on Node's built-in runner (`node:test`)  |\n| `pnpm format:check` | Prettier check                                      |\n| `pnpm format`       | Prettier write                                      |\n\n## Configuration\n\n| Env var                            | Default       | Purpose                                                               |\n| ---------------------------------- | ------------- | --------------------------------------------------------------------- |\n| `DATABASE_URL`                     | _required_    | Postgres connection string                                            |\n| `STATEBOARD_BASE_URL`              | _required_    | Public URL the app reaches itself at                                  |\n| `BETTER_AUTH_SECRET`               | _required_    | 32-byte base64 secret for session cookies (`openssl rand -base64 32`) |\n| `KEYCLOAK_ISSUER`                  | _required_    | Realm URL, e.g. `https://keycloak.example.com/realms/acme`            |\n| `KEYCLOAK_CLIENT_ID`               | _required_    | Confidential client id                                                |\n| `KEYCLOAK_CLIENT_SECRET`           | _required_    | Client secret                                                         |\n| `STATEBOARD_ALLOWED_EMAIL_DOMAINS` | _empty_ (any) | Comma-separated allowlist                                             |\n| `STATEBOARD_DEFAULT_ROLE`          | `editor`      | Role given to non-first sign-ins                                      |\n| `STATEBOARD_DATA_DIR`              | `./data`      | Root of upload storage                                                |\n| `PORT`                             | `3000`        | HTTP port                                                             |\n| `NODE_EXTRA_CA_CERTS`              | _unset_       | Path to a PEM CA bundle to trust for an internal/self-signed IdP cert |\n\n## Roadmap\n\n- **v0 (the wedge)** ✅: manual upload, region tagging, three states, share link, single user\n- **v1 (team-ready)** ✅ (you are here): multi-user, OIDC, audit log, Postgres\n- **v1.x (still scope-OK)**: headless capture from URL, Jira issue linking, custom states\n- **v2 (make it living)**: scheduled re-capture, time-travel / diff view, two-way Jira sync, Slack, Notion/Confluence embed\n- **v3 (defensible)**: DOM region-detection, journey views, portfolio rollup, SAML, audit-log UI, template gallery\n\nFull narrative in [`/docs/roadmap`](https://saschb2b.github.io/stateboard/docs/roadmap). Live status: [milestones](https://github.com/saschb2b/stateboard/milestones) for the per-issue rollup, or the [project board](https://github.com/users/saschb2b/projects/1) for a Kanban / Roadmap view.\n\nThe temptation will be to chase roadmap-tool features. We won't. The lane is **screens, regions, states, and the integrations that keep them honest**, and nothing else.\n\n## License\n\nMIT. Use it, fork it, ship it inside your product, sell consulting around it. The license means what it says.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaschb2b%2Fstateboard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsaschb2b%2Fstateboard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaschb2b%2Fstateboard/lists"}