PE turnaround execution observatory that connects strategic intent to ground-level work patterns. Surfaces misalignment, drift, and dark work attribution before they hit the P&L. Designed to be rapidly deployable into any portfolio company to measure whether teams execute against strategy or just go through the motions.
- Backend: Java 21, Spring Boot 3.2.5, Spring Data JPA, PostgreSQL 16, Flyway
- Frontend: React 18, TypeScript (strict), Vite, TanStack Query, Zustand, Tailwind CSS, Recharts, Module Federation (
@originjs/vite-plugin-federation) for host-app integration - LLM: OpenAI (default), Anthropic fallback — configurable model and base URL
- Infrastructure: Docker Compose (PostgreSQL + API + static UI), nginx inside the frontend image (SPA +
/apireverse proxy to the backend — there is no separate nginx Compose service) - Observability: Spring Boot Actuator (
health,info,metrics); JSON logs on therailwayprofile (Logstash encoder) - Testing: JUnit 5, Testcontainers, Vitest, Testing Library, MSW
| Piece | Role |
|---|---|
| PostgreSQL 16 | Primary datastore; Flyway migrations on backend startup |
| Backend | Spring Boot on port 8080 (server.port) |
| Frontend image | nginx:alpine serves the Vite build from /usr/share/nginx/html and proxies /api/* to the backend (BACKEND_URL in the frontend container — http://backend:8080 under Compose) |
Compose project name is compass (see name: in the YAML files).
Ports
| Service | Local dev (native) | docker compose |
|---|---|---|
| UI | http://localhost:3001 (Vite dev server) |
http://localhost:3000 (container 80 → host 3000) |
| API | http://localhost:8080 |
http://localhost:8080 |
| PostgreSQL | 127.0.0.1:5432 |
127.0.0.1:5432 |
Health check: GET http://localhost:8080/actuator/health
- Docker and Docker Compose (for full stack or DB-only)
- Java 21 + Gradle wrapper (for running the backend outside Docker)
- Node.js 20+ with pnpm (Corepack:
corepack enablethen usepnpm— lockfile ispnpm-lock.yaml)
Builds backend and frontend images, starts PostgreSQL, API, and UI.
docker compose up --buildOpen http://localhost:3000. The browser talks to nginx on 3000; API calls go to /api/..., which nginx forwards to the backend container.
First startup runs Flyway and, with the default local profile in Compose, seeds the small demo dataset (see Seed data).
To stop: Ctrl+C or docker compose down. To reset the DB volume: docker compose down -v (destructive).
Keeps Postgres in Docker; run Spring Boot and Vite locally for fast reload.
# 1) Start PostgreSQL only
docker compose -f docker-compose.dev.yml up -d
# 2) Backend — uses application-local.yml (localhost:5432, seed on)
cd backend
./gradlew bootRun
# API: http://localhost:8080
# 3) Frontend (separate terminal)
cd frontend
pnpm install
pnpm dev
# UI: http://localhost:3001 — Vite proxies /api to :8080Optional: copy .env.example to .env in the repo root and export variables your shell (or IDE) should pass to the JVM — Spring Boot does not auto-load .env; use export, direnv, or IDE run configuration. Backend picks up standard env vars (see below). For the Vite app, only variables prefixed with VITE_ are exposed to the client.
The default pnpm build produces the standalone app. To emphasize the federated remote entry (for a parent/host app that consumes this module):
cd frontend
pnpm run build:remoteValues below match backend/src/main/resources/application*.yml. Prefer env vars in production; local defaults live in application-local.yml when SPRING_PROFILES_ACTIVE=local.
Backend — core
| Variable | Typical / default | Description |
|---|---|---|
SPRING_PROFILES_ACTIVE |
local (dev), railway (hosted) |
Loads application-{profile}.yml |
SPRING_DATASOURCE_URL |
jdbc:postgresql://localhost:5432/compass (local file) |
JDBC URL; Compose sets jdbc:postgresql://db:5432/compass |
SPRING_DATASOURCE_USERNAME |
compass |
|
SPRING_DATASOURCE_PASSWORD |
compasslocal |
|
DATABASE_URL |
— | Railway: JDBC URL injected by the platform (see application-railway.yml) |
Backend — product
| Variable | Typical / default | Description |
|---|---|---|
COMPASS_SEED_ENABLED |
true (local profile), false (Railway default) |
Run demo seed on startup |
COMPASS_SEED_OBSERVATORY |
false |
Large seed: 3 orgs, ~150 users, 12 weeks (~30s) |
COMPASS_CORS_ALLOWED_ORIGINS |
Local: http://localhost:3000,http://localhost:3001 |
Comma-separated origins; must be set to your real frontend origin in production or CORS will block |
COMPASS_JWT_ISSUER |
— | Production (railway): JWT issuer |
COMPASS_JWT_PUBLIC_KEY |
— | Production: PEM for RS256 validation |
Backend — LLM
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
— | Primary provider |
ANTHROPIC_API_KEY |
— | Used when OpenAI is not configured or as configured by code paths |
LLM_MODEL |
gpt-4.1-nano |
Model name |
LLM_BASE_URL |
https://api.openai.com/v1 |
OpenAI-compatible API base |
Template / fallback narratives are used when keys are absent so the app still runs.
Frontend (Vite)
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE_URL |
(empty) | API base URL for Axios. Empty = relative /api (correct behind Compose nginx or Vite proxy). Set to an absolute URL only if the API is on another origin without a proxy |
Frontend container (Docker / production)
| Variable | Default in image | Description |
|---|---|---|
BACKEND_URL |
http://backend:8080 |
Upstream for location /api/ in nginx. Under Compose, hostname backend resolves. On split hosts (e.g. Railway), set this to your backend’s public origin, e.g. https://your-api.up.railway.app |
PostgreSQL 16 — with docker-compose.dev.yml or full docker-compose.yml, the server is on 127.0.0.1:5432, database compass, user compass, password compasslocal. Flyway runs automatically when the backend starts; no manual DDL.
Small seed (default when COMPASS_SEED_ENABLED=true with the local profile): 1 org (Meridian Manufacturing), 10 users, 2 Rally Cries, 4 defining objectives, 5 outcomes, 3 weeks of commitments and reconciliation records.
Observatory seed (COMPASS_SEED_OBSERVATORY=true): 3 orgs, ~150 users, 12 weeks of generated data. Allow ~30 seconds on boot.
# Backend — Docker must be running (Testcontainers)
cd backend && ./gradlew test
# Frontend
cd frontend && pnpm testOptional: cd frontend && pnpm run typecheck / pnpm run lint
The platform is organized around three primary views gated by role:
My Week (All Users) — Individual contributor hub. Weekly commitment CRUD with drag-to-reorder prioritization, Rally Cry linkage, Chess categorization (Strategic / Operational / Defensive / Capability Building), task bullet breakdowns, and completion horizon estimates. Manages a per-user weekly lifecycle state machine: DRAFT → LOCKED → RECONCILING → RECONCILED. Unfinished items carry forward automatically with full lineage tracking.
My Team (Manager+) — Team rollup of all direct reports' commitments with assignment signals, alignment gap detection, Rally Cry coverage cards, and rolling 12-week work history per team member.
The Briefing (Director+) — Director-level intelligence with org-unit health signals (GREEN/YELLOW/RED), drill-down navigation (Rally Cry → Team → IC), AI-generated narrative summaries, and KPI metrics strip.
My Story — Personal growth area tracking with progress visualization, AI-generated pattern insights, and auto-generated resume bullets tied to personal alignment. Dual-axis view: personal growth + organizational alignment.
Close My Week — End-of-week reconciliation ritual. Planned vs actual comparison for every commitment with structured displacement tracking, unplanned work entry, and personal reflection.
Commitment History — Rolling 12-week view of past commitments with carry-forward lineage and visual timeline showing a commitment's journey across cycles.
Observatory (VP+) — Program health dashboard with KPI strip (Rally Cry coverage, completion rate, carry-forward count, displacement rate), execution trend charts, configurable drift detection (alignment / velocity / coverage drift with severity escalation: emerging → sustained → structural), team trajectory small multiples, org-chart heatmap, and data integrity signals detecting gaming patterns.
Portfolio (VP+) — Cross-portfolio company comparison with sparkline trends, side-by-side metrics, AI-powered chat sidebar for asking questions about portfolio health, and drill-down into individual orgs.
Strategy (Director+) — RCDO hierarchy builder: Rally Cry → Defining Objective → Outcome. Kanban-style drag-and-drop with CRUD modals and owner assignment.
AI-powered features throughout the platform:
- Briefing narratives — Auto-generated prose summaries of team and program state
- IC insights — Pattern analysis of commitment history tied to growth goals
- Portfolio chat — Conversational interface for portfolio health questions
- Narrative feedback — Users can thumbs-up/down AI outputs to improve quality
Uses OpenAI by default (LLM_MODEL / LLM_BASE_URL) with Anthropic available. Hardcoded template narratives serve as fallback when no API key is configured.
- Settings — Profile management, admin panel (Manager+) for user/role/team management, organization config (VP+) for drift thresholds and cost bands
- Methodology — Built-in help page explaining platform concepts and best practices
- Landing Page — Product introduction with role cards, feature preview, and stats
- Architecture Page — Visual system architecture diagram
JWT-based authentication with role-based visibility:
| Role | Sees |
|---|---|
| EMPLOYEE | Own commitments only |
| MANAGER | Own + direct reports |
| DIRECTOR | Full org subtree |
| VP | Full org subtree + portfolio + observatory |
| EXECUTIVE | Entire organization |
| ANALYST | Read-only within scoped boundary |
The local Spring profile uses DevTokenValidator so you can develop without a real IdP; production (railway) uses RS256 validation when JWT properties are set.
frontend/ React 18 SPA (Vite + TypeScript strict)
src/
features/
my-week/ Weekly commitment hub (V2 IC experience)
my-story/ Personal growth & narrative
my-team/ Manager team rollup & analytics
briefing/ Director-level AI briefings
observatory/ Executive health, drift, heatmap
portfolio/ Cross-company comparison & AI chat
strategy/ RCDO hierarchy builder
reconciliation/ Planned vs actual + displacement
weekly-lifecycle/ Cycle state machine UI
commitment-history/ Rolling history & lineage
growth-areas/ Personal growth area CRUD
settings/ Profile, admin, org config
methodology/ Help & best practices
landing/ Product intro page
architecture/ System architecture page
api/ Axios-based API modules (15 files)
hooks/ React Query hooks + auth context
components/ Shared UI (Layout, nav, date range selector)
backend/ Spring Boot 3.2.5 (Java 21)
domain/
commit/ Commitment, TaskBullet, ChessCategory
cycle/ Cycle state machine + carry-forward
reconciliation/ Reconciliation records + displacement
dashboard/ Manager analytics, alignment, RCDO coverage
observatory/ Executive analytics, drift detection,
displacement aggregation, portfolio health
briefing/ Director narratives + LLM integration
icinsights/ IC narrative generation
growth/ Growth area CRUD
rcdo/ Rally Cry > Defining Objective > Outcome
user/ AppUser, Org, roles, team activation
importexport/ CSV importers (users, RCDO, categories, commitments)
security/ JWT auth, role-based visibility, analyst scoping
audit/ Append-only audit trail
seed/ Seed data generators (small + observatory-scale)
- Weekly cycle as first-class entity — Not derived from dates; each user manages their own lifecycle independently
- State machine enforcement — DRAFT → LOCKED → RECONCILING → RECONCILED, no backdating
- Carry-forward as lineage — New row in next cycle with parent reference, preserving full history
- Displacement over surveillance — Detects misalignment through work pattern analysis, not activity monitoring
- Pattern-based signals — Individual data points invisible at executive level; only recurring themes surface via clustering
- No dollar amounts required — Cost weighting works with role tiers as proxy; dollar rates are optional
- Configurable thresholds — Drift detection sensitivity, alignment targets, and warning levels are adjustable per-org
There is no railway.toml in this repo; configure services in the Railway dashboard (or CLI).
- Postgres — Use Railway’s PostgreSQL plugin.
DATABASE_URLis injected for the backend. - Backend service — Build from
backend/Dockerfile. Set:SPRING_PROFILES_ACTIVE=railwayCOMPASS_CORS_ALLOWED_ORIGINS= your frontend public origin (e.g.https://your-app.up.railway.app)COMPASS_JWT_ISSUER/COMPASS_JWT_PUBLIC_KEYfor production authCOMPASS_SEED_ENABLED=falsefor normal operation (enable briefly if you need a first-time demo seed)
- Frontend service — Build from
frontend/Dockerfile. SetBACKEND_URLto the backend’s public base URL (scheme + host, no trailing path), e.g.https://your-api.up.railway.app, so nginx can proxy/apito the API.
Connection pool size is 10 on Railway vs 5 for local (application-railway.yml vs application-local.yml).
Same as Option A: multi-stage backend/Dockerfile (Eclipse Temurin 21) and frontend/Dockerfile (Node build → nginx). Override BACKEND_URL on the frontend container if the API hostname is not http://backend:8080.
| Service | Local dev | Docker Compose |
|---|---|---|
| Frontend | localhost:3001 |
localhost:3000 → container 80 |
| Backend | localhost:8080 |
localhost:8080 |
| PostgreSQL | localhost:5432 |
127.0.0.1:5432 |