A self-hosted web application for kanban teams. Connects to a self-hosted Jira Data Center instance via a service-account PAT, synchronizes issue data on a local PostgreSQL database, and serves flow visibility, aging analysis, and story-count Monte Carlo forecasts entirely from local read models.
Hosted Vercel beta support is available for Jira Cloud with Clerk Organizations, Atlassian OAuth, Prisma Postgres, and Vercel Queues. The Docker/self-hosted path remains the default; see docs/hosted-vercel.md for hosted setup and release gates.
- Jira sync — scheduled and manual synchronization of board issues, lifecycle events, and hold periods, with graceful worker shutdown canceling in-flight syncs and stale queued/running syncs auto-failed after 60 minutes so scopes can recover without manual database cleanup
- Admin configuration — create and edit Jira connections plus create, edit, and delete board/flow scope mappings from the admin UI
- Current flow view — scatter plot of active work items with percentile-based aging zones and on-hold classification
- Work-item detail — per-item lifecycle timeline and hold period breakdown
- Monte Carlo forecasting — story-count "when will we finish?" and "how many by a date?" simulations backed by local throughput history
| Requirement | Version |
|---|---|
| Node.js | 24 LTS |
| pnpm | 10+ |
| Docker | any recent version |
| Jira Data Center | 8.14+ |
A Jira service-account PAT with read access to the target board, its issues, and changelog history is required. The PAT is entered through the admin UI and stored encrypted — it is never written to environment files.
apps/web # Next.js UI and HTTP API (port 3000 by default)
apps/worker # Scheduled sync, manual refresh jobs, projection rebuilds
packages/db # Prisma schema, migrations, and database helpers
packages/analytics
packages/jira-client
packages/shared
pnpm installCopy .env.example to .env and fill in the values:
cp .env.example .envRequired variables:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/agile_tools
ENCRYPTION_KEY=<32+ character random key — never reuse across environments>
SESSION_SECRET=<32+ character random key>
DEFAULT_SYNC_INTERVAL_MINUTES=10
LOG_LEVEL=debugRecommended when you run the production web container directly on http://localhost without TLS termination:
ALLOW_LOOPBACK_HTTP_BYPASS=true
ALLOW_LOCAL_BOOTSTRAP=trueOptional self-host branding variables can set the product name, title suffix,
description, colors, logo, and favicon. Logo and favicon variables accept
https:// URLs, app-root-relative paths such as /brand/logo.svg, or data
image URLs up to 256 KiB using image/svg+xml, image/png, image/x-icon, or
image/vnd.microsoft.icon. Data URLs are useful when deploying a prebuilt
Docker image without mounting or baking brand assets:
APP_BRAND_LOGO_LIGHT_URL=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0...
APP_BRAND_FAVICON_URL=data:image/png;base64,iVBORw0KGgoAAA...Self-hosted SSO is optional. Leave AUTH_PROVIDER=local_session to keep the
existing unauthenticated landing page, read-only fallback option, and local
admin bootstrap cookie flow. To require organization sign-in instead, set
AUTH_PROVIDER=oidc and configure the OIDC_* variables in .env.example.
Set OIDC_AUTO_LOGIN=true only when browser page requests without a session
should immediately redirect to SSO; API routes, metrics, static assets, and
OIDC callback/logout routes are excluded. OIDC_SESSION_MAX_AGE_SECONDS
controls Agile Tools' OIDC cookie max age, while the effective authenticated
lifetime remains capped by the IdP token and revocation policy. When
AUTH_PROVIDER=oidc, legacy local/bootstrap agile_session cookies are not
accepted; admins should be mapped through OIDC_ADMIN_EMAILS or
OIDC_ADMIN_CLAIM.
For setup, IdP claim mapping, and existing workspace migration, see
docs/self-hosted-oidc-sso.md. For a local
Keycloak walkthrough, see docs/local-oidc-sso.md.
Hosted Vercel beta remains Clerk-only.
docker compose up -d postgrespnpm --filter @agile-tools/db prisma:migratepnpm --filter @agile-tools/web devpnpm --filter @agile-tools/worker devOpen http://localhost:3000 and navigate to Admin → Jira Connections to configure the first connection.
The web runtime exposes public OpenTelemetry metrics in Prometheus text format at http://localhost:3000/metrics.
The worker runtime exposes the same scrape format at http://localhost:9464/metrics when run from Docker Compose.
Scrape either endpoint from Prometheus, Grafana Alloy, or another compatible collector. If the app is reachable outside a trusted
network, restrict /metrics at the reverse proxy or network layer because metric labels can reveal runtime details.
Worker metrics bind to METRICS_HOST and METRICS_PORT. METRICS_PORT takes precedence; if it is unset, the worker
uses PORT so Kubernetes-style environments can assign the scrape port with the standard platform variable, then falls
back to 9464. The Docker Compose files publish the worker scrape port from WORKER_METRICS_PORT, defaulting to 9464.
Metrics include runtime scrape/process data, standard OpenTelemetry http.server.request.duration histograms for web
requests, standard OpenTelemetry http.client.request.duration histograms for outbound Jira REST calls, forecast and flow
analytics reads, manual sync enqueues, worker job and sync runs, queue depth snapshots, and Prisma query durations. In
Prometheus text, the HTTP series are emitted as http_server_request_duration and http_client_request_duration with unit
s, plus count, sum, and second-based bucket series; derive request and error rates from each metric's _count series
filtered by http_response_status_code or error_type. Jira client metrics also include low-cardinality
agile_tools_jira_operation and agile_tools_jira_result labels. OIDC auth events are counted in
agile_tools_oidc_auth_events_total with low-cardinality event, result, and reason labels. Labels intentionally
avoid workspace IDs, issue keys, user data, raw URLs, SQL, JQL, OIDC issuers, subjects, or email addresses.
The repository now ships a single multi-stage Docker image that contains both runtime roles:
webfor the Next.js UI and HTTP APIworkerfor scheduled sync and projection jobs
This is the recommended compromise for this monorepo: build one image artifact, then run separate containers for the web and worker roles. That stays aligned with Docker's one-service-per-container guidance while still keeping artifact management simple.
docker build -t agile-tools:local .If you are consuming a release image instead of building from source, use docker-compose.consumer.yml. It pulls ${AGILE_TOOLS_IMAGE:-ghcr.io/rubiss-projects/agile-tools:latest} directly from GitHub Container Registry with no local retag step.
docker compose -f docker-compose.consumer.yml up -d postgres
docker compose -f docker-compose.consumer.yml --profile bootstrap run --rm bootstrap
docker compose -f docker-compose.consumer.yml --profile runtime up -dIf you want to pin a specific release instead of following latest, set AGILE_TOOLS_IMAGE in your shell or .env, for example ghcr.io/rubiss-projects/agile-tools:v0.1.1.
The published image runs in production mode, so the dev demo bootstrap remains disabled there. For local image hosting, set ALLOW_LOCAL_BOOTSTRAP=true and use the built-in local admin bootstrap action from / or /admin/jira. The bootstrap flow is intended for loopback hosts such as localhost and 127.0.0.1.
docker run --rm -p 3000:3000 --env-file .env \
-e ALLOW_LOOPBACK_HTTP_BYPASS=true \
-e ALLOW_LOCAL_BOOTSTRAP=true \
-e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/agile_tools \
agile-tools:local webdocker run --rm --env-file .env \
-e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/agile_tools \
agile-tools:local workerdocker run --rm --env-file .env \
-e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/agile_tools \
agile-tools:local bootstrapThe root compose file keeps postgres as the default local dependency and exposes a one-off bootstrap job plus the application runtime containers behind profiles:
docker compose up -d postgres
docker compose --profile bootstrap run --rm bootstrap
docker compose --profile runtime up --build -dThe bootstrap role runs Prisma migrations and pg-boss schema migrations from the same image artifact. The image also supports an all command for running both processes in one container, but that is a convenience mode rather than the recommended production shape.
Some managed platforms inject the PostgreSQL connection string under an autogenerated variable name (for example XYZ_POSTGRESQL_URI) that the platform controls and the application cannot rename. To support that, the web and worker runtimes read the database URL from a configurable source:
- If
DATABASE_URL_ENV_VARis set, the value of the variable it names is used as the database URL (for exampleDATABASE_URL_ENV_VAR=XYZ_POSTGRESQL_URIreadsprocess.env.XYZ_POSTGRESQL_URI). - Otherwise the runtime falls back to
DATABASE_URL, which is the default for local, dev, and docker-compose setups.
If DATABASE_URL_ENV_VAR is set but the referenced variable is missing or empty, the application fails at startup with a clear configuration error. The same resolution is applied by the container entrypoint, so the bootstrap role (Prisma migrate and pg-boss migrate) honors DATABASE_URL_ENV_VAR as well.
The local compose runtime defaults ALLOW_LOOPBACK_HTTP_BYPASS=true for the web container so http://localhost:3000 works without an extra reverse proxy.
It also defaults ALLOW_LOCAL_BOOTSTRAP=true, which enables a loopback-only local admin bootstrap action on the unauthenticated home and admin pages.
If your environment depends on a custom npm registry, proxy, or relaxed TLS setting, the source-build compose path now forwards optional build-time variables such as NPM_CONFIG_REGISTRY, COREPACK_NPM_REGISTRY, HTTP_PROXY, HTTPS_PROXY, NO_PROXY, and NODE_TLS_REJECT_UNAUTHORIZED from the invoking shell. The build uses the repository's pinned packageManager field through Corepack. By default, Corepack uses NPM_CONFIG_REGISTRY, but you can set COREPACK_NPM_REGISTRY separately when package-manager downloads must come from a different Artifactory remote than normal package installs.
The consumer compose file uses the same service layout and defaults, but it pulls the published image from GHCR instead of building from the local checkout.
Open http://localhost:3000 and navigate to Admin → Jira Connections to configure the first connection.
If you are running the production image locally and do not have an upstream workspace session provider, open http://localhost:3000/admin/jira and use Create local admin session and continue.
If you want a real Jira target on the same machine, use docker-compose.jira.yml. It runs a single-node Jira Data Center instance plus a dedicated PostgreSQL database, isolated from the main Agile Tools stack.
docker compose -f docker-compose.jira.yml up -d
docker compose -f docker-compose.jira.yml logs -f jiraFor a full clean rerun that also clears generated Jira seed output and the local Agile Tools admin setup state, use:
node docker/reset-local-jira-stack.mjsAfter the Jira setup wizard is complete, you can also run the optional seed job:
docker compose -f docker-compose.jira.yml --profile bootstrap run --rm jira-bootstrapSee docs/local-jira-testing.md for the reset flow, browser-driven first-run wizard guidance, optional bootstrap env variables, and the minimum project, board, PAT, and permission checklist needed to connect Agile Tools to the local Jira instance.
The repository includes a tag-driven release workflow that publishes the runtime image to GitHub Container Registry and creates a GitHub Release with generated notes.
- Push a semver tag to publish a release:
vX.Y.Zfor stable releases orvX.Y.Z-rc.1for prereleases - Cut release tags from a green commit that is already contained in the default branch history
- The workflow publishes
ghcr.io/<owner>/<repo>tags for the release version and digest - Stable releases also receive
latestandX.Y, and stable releases starting atv1.0.0also receive theXtag - You can also run the workflow manually from the Actions tab for an existing tag
git tag v0.1.0
git push origin v0.1.0For prereleases, use a semver prerelease suffix such as v0.1.0-rc.1.
- The workflow uses the repository
GITHUB_TOKENto push toghcr.io, so GitHub Actions package publishing must be allowed for the repository or organization - If a package with the same
ghcr.io/<owner>/<repo>name was previously pushed outside Actions and is not linked to this repository, connect it to the repository or recreate it so the workflow token can write to it - The release workflow publishes a multi-architecture image for
linux/amd64andlinux/arm64/v8 - CI validates that each published runtime shape bundles the matching Prisma schema and query engines instead of relying on runtime downloads
# Install all workspace dependencies
pnpm install
# Type-check all packages
pnpm typecheck
# Lint all packages (zero warnings enforced)
pnpm lint
# Run unit tests
pnpm test
# Run contract tests
pnpm test:contract
# Run integration tests (requires Docker for Testcontainers)
pnpm test:integration
# Run end-to-end tests (requires a running web app)
pnpm test:e2e
# Run performance benchmarks (requires Docker for Testcontainers)
pnpm test:perf
# Build all packages
pnpm build- See
CONTRIBUTING.mdfor local setup, validation, and pull request expectations. - See
SUPPORT.mdfor question routing and support expectations. - See
SECURITY.mdfor responsible vulnerability reporting. - See
CODE_OF_CONDUCT.mdfor collaboration standards.
See specs/001-kanban-flow-forecasting/quickstart.md for the full configuration and validation guide, including instructions for creating a Jira connection, defining a flow scope, triggering the first sync, and verifying all three primary user journeys.
See specs/001-kanban-flow-forecasting/plan.md for the technical design, data model, and delivery strategy.