dashboard/README.md

Ployz Dashboard

Ployz Cloud: web UI, backend, and durable workflows. It authors each Deploy Intent and drives Clusters through Machine RPC; it is not runtime authority. The deployment engine and SDK live in core/.

Develop

Run commands from dashboard/. Install Node and pnpm versions from package.json, plus Rust (rustup) for the locally linked SDK (see core).

sh
1pnpm install --frozen-lockfile
2cp .env.example .env
3docker compose up -d
4pnpm db:migrate
5pnpm dev
6# In a second terminal (rebuild after worker code changes):
7pnpm exec vite build --config vite.worker.config.ts
8PORT=3001 INNGEST_DEV=1 pnpm start:worker

Configure .env before running migrations or starting the app. pnpm dev builds the SDK and starts the app through Portless. pnpm pr:check runs CI's checks, for reproducing a CI failure locally.

Compose uses the stable project name ployz-cloud so moving the checkout keeps the same database volume. If an existing local stack uses another project name, retain it with docker compose -p <existing-name>.

Environment

Web and worker read the same variables and exit with a ConfigError naming what is missing or invalid.

VariableRequiredNotes
DATABASE_URLyesPostgres URL
APP_URLyespublic URL of web
BETTER_AUTH_SECRETyesauth session secret
APP_ENCRYPTION_SECRETyes32+ characters; encrypts stored credentials, keep it stable
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRETyesGitHub OAuth App (sign-in)
GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_WEBHOOK_SECRETyesGitHub App (repository access)
INNGEST_EVENT_KEY, INNGEST_SIGNING_KEYyesshared with the Inngest server
INNGEST_BASE_URL, INNGEST_CONNECT_GATEWAY_URLnoInngest API and Connect gateway; local dev defaults otherwise
BETTER_AUTH_TRUSTED_ORIGINS, PORTnoPORT defaults to 3000
POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_PRODUCT_ID, POLAR_SERVERnoall or none; none disables billing and every Organization is unlimited
UPTIME_ENABLEDnotrue on the dashboard when an uptime checker (uptime/) runs; unset hides uptime
POSTHOG_KEY, POSTHOG_HOSTnoproduct analytics, Ployz-hosted only; no key disables it. POSTHOG_HOST defaults to PostHog US Cloud; pnpm posthog:backfill sends existing users and Organizations once

Build and deploy

pnpm build compiles the native SDK and config WASM, then builds the web and Connect worker into .output/. Dockerfile.cloud packages both in one image; the root Dockerfile builds the same image from source, so Railway can deploy any branch without CI:

ProcessStart commandHealthcheck
Webnpm start/api/health
Workernpm run start:worker/ready

Run one worker alongside the web service. Both use the same application variables and database; only web needs public ingress. The worker uses INNGEST_BASE_URL for the Inngest API and, for a self-hosted gateway, set INNGEST_CONNECT_GATEWAY_URL=ws://inngest.railway.internal:8289/v0/connect. The equivalent local dev defaults are ports 8288 and 8289. Use different PORT values when running both processes directly on the same host.

Connect registers the ployz-cloud functions automatically; web exposes no /api/inngest route. Both processes run the same image.

On SIGTERM/SIGINT, Connect stops accepting new steps and finishes active steps before the worker disposes database and SDK resources. Set Railway worker RAILWAY_DEPLOYMENT_DRAINING_SECONDS=1800 (or a longer operational allowance); on Docker use the corresponding stop_grace_period. This is a shutdown grace period, not a deployment execution timeout. A crash or forced kill can still leave remote effects unknown; interrupted deployment steps are not blindly retried.

Run migrations once before a rollout, not independently on each process.

See DESIGN.md for product design and CONTEXT.md for the Dashboard glossary.

Cloud image deployment

The existing Cloud workflow reuses the matching SDK artifact (and the shared kache/R2 compiler cache on misses), builds both entrypoints, and packages Dockerfile.cloud. The final image smoke test loads the native SDK inside the runtime container. CI uses Ubuntu 24.04; the runtime uses Node 24 on Debian Trixie so its glibc supports that native artifact.

Railway builds web and worker from this repository's root Dockerfile on every push to main, so both run the same commit within a build of each other. Web runs migrations before it deploys; worker overrides the start command (node .output/worker/index.mjs) and keeps its /ready healthcheck and 30-minute drain.

Each release tag also publishes ghcr.io/getployz/ployz-cloud:<tag> from the same build, and attaches ployz-cloud-compose.yml (pinned to that tag) and ployz-cloud.env.example to the GitHub release.

Self-host

Self-hosted Cloud runs the released image as web and worker beside Inngest, Redis, and Postgres (self-host/compose.yml). You bring your own GitHub apps; billing is off, so Organizations are unlimited. The relay, Hosted DNS, installer, and release binaries remain Ployz-hosted.

1browser ──TLS proxy──► web :3000 ──┐
2GitHub webhooks ───────► web ├─► Postgres (ployz_cloud, inngest)
3 worker ◄───┤─► Inngest :8288 API / :8289 Connect ─► Redis

The image is linux/amd64. Put a TLS proxy in front of web's WEB_PORT; Inngest's UI is bound to 127.0.0.1:8288 for operator use only.

1. Create the GitHub apps

Replace https://cloud.example.com with your APP_URL.

OAuth App (sign-in) at GitHub → Settings → Developer settings → OAuth Apps:

FieldValue
Homepage URLhttps://cloud.example.com
Authorization callback URLhttps://cloud.example.com/api/auth/callback/github

Copy the Client ID to GITHUB_CLIENT_ID and a new client secret to GITHUB_CLIENT_SECRET.

GitHub App (repository access) at GitHub → Settings → Developer settings → GitHub Apps (or under your organization):

FieldValue
Homepage URLhttps://cloud.example.com
Webhook URLhttps://cloud.example.com/api/github/webhook (Active)
Webhook secretopenssl rand -hex 32, also GITHUB_APP_WEBHOOK_SECRET
Repository permissionsContents: Read-only, Checks: Read and write, Metadata: Read-only, Actions: Read and write, Pull requests: Read-only
Subscribe to eventsPush, Check suite, Workflow run, Pull request

Actions: Read and write and the Workflow run event are for GitHub builds: Cloud checks for the ployz-build.yml workflow and dispatches it. Installations created before you add them keep building on servers until their owner approves the new permissions in GitHub.

Pull requests: Read-only, Checks: Read and write and the Pull request event are for PR Environments: Cloud makes one per pull request and posts a "Ployz · ready to merge" check. Add them only after upgrading to a release that handles pull request deliveries; older releases answer those deliveries with an error. Installations created before you add them get no PR Environments until their owner approves the new permissions in GitHub.

Installation events are delivered without subscribing. Copy the App ID to GITHUB_APP_ID, the URL name from github.com/apps/<slug> to GITHUB_APP_SLUG, and a generated private key (the whole PEM, quoted) to GITHUB_APP_PRIVATE_KEY.

2. Configure and start

Download ployz-cloud-compose.yml as compose.yml and ployz-cloud.env.example as .env from the release into one directory, then fill in .env. Every uncommented variable is required; web and worker exit with a ConfigError if one is missing. Leave all POLAR_* and POSTHOG_* variables unset.

sh
1docker compose run --rm web npm run db:migrate # explicit, once per install/upgrade
2docker compose up -d
3docker compose ps # worker turns healthy once Connected

Neither web nor worker migrates on boot. The worker's healthcheck is /ready: 200 while its Inngest Connect connection is active, 503 otherwise. It is not a database, schema, or GitHub check. Compose gives the worker a 30-minute stop_grace_period so active steps drain.

3. Sign in, then install the GitHub App

Sign in to Cloud with GitHub first, then install the GitHub App as that same GitHub user (from Cloud or github.com/apps/<slug>). Installation webhooks are only attached to a GitHub account already linked in Cloud.

Upgrade

Replace compose.yml with the new release's asset (or bump PLOYZ_CLOUD_VERSION), then:

sh
1docker compose pull
2docker compose run --rm web npm run db:migrate
3docker compose up -d