ReferenceOperating

Deployment

Deploy the UI, API worker, docs, cron schedule, and PostgreSQL database on Ploy.

Work on Repeat is prepared as four workspace apps:

  • apps/ui — one Next.js service for marketing, the blog, authentication, onboarding, and the dashboard;
  • apps/api — Hono API, run execution, and scheduler;
  • apps/db — Drizzle schema, migrations, and repositories; and
  • apps/docs — this Fumadocs Next.js service.

Three of them are deployable and each carries its own ploy.yaml. Ploy detects the monorepo automatically, installs from the repository root so workspace dependencies resolve, and runs each build from its base directory.

Projects

AppkindEntryNotes
apps/uinextjsApp RouterMarketing, blog, auth, and dashboard
apps/docsnextjsApp RouterThis documentation site
apps/apiworkersrc/worker.tsRuns on workerd, with a cron trigger

apps/api/src/worker.ts is the workerd entrypoint. src/server.ts remains the Node entrypoint used by pnpm dev and by any non-Ploy host. The difference is how background work happens: the Node process runs a polling run-worker, while on Ploy the cron trigger drives the same operations tick and a manually dispatched run executes on the request's waitUntil.

Ploy builds workers with nodejs_compat_v2, which is what lets Postgres.js open a TCP connection to your database from the worker.

Environment contract

Set these on the API project:

API_ORIGIN
UI_ORIGIN
DATABASE_URL
NODE_ENV=production
DEMO_MODE=false
BETTER_AUTH_SECRET
ENCRYPTION_KEY
SCHEDULER_SECRET
LLM_GATEWAY_API_KEY
LLM_GATEWAY_DEFAULT_MODEL

Set API_ORIGIN and DOCS_ORIGIN on the UI project.

ENCRYPTION_KEY must be a base64-encoded 32-byte value, and BETTER_AUTH_SECRET and SCHEDULER_SECRET must each be at least 32 characters. Secrets should differ in every environment. The API refuses to start in production when fixture mode is enabled or LLM_GATEWAY_API_KEY is absent.

Every variable, including the optional OAuth providers, is documented in Environment variables.

Database

Ploy does not host PostgreSQL, so point DATABASE_URL at your own instance and run pnpm db:migrate as a release step before the new version takes traffic. Migrations are ordered and idempotent; the organizations-and-projects migration preserves existing rows rather than recreating tables.

Scheduler

The apps/api project declares a one-minute cron trigger:

apps/api/ploy.yaml
cron:
  OPERATIONS_TICK: "* * * * *"

Each tick claims due routines, executes queued runs, and retries webhook deliveries. Claims are atomic, so overlapping ticks never duplicate a run.

POST /internal/scheduler/tick with X-Scheduler-Secret remains available for hosts that drive the schedule externally. On a deployment without a scheduler configured it returns 503 scheduler_unavailable.

Without a tick, nothing runs

The scheduler is the only thing that claims due routines. If activated routines never execute, confirm the cron trigger is firing before looking anywhere else.

Before production traffic

Run database migrations.
Verify UI-to-API cookie and CORS origins.
Confirm the cron trigger is firing and advancing nextRunAt.
Complete a live LLMGateway smoke test.
Complete one OAuth round trip per configured provider.

Confirm automatic execution for a test MCP tool and inspect its trace.

Verify webhook signatures from the production receiver.

On this page

Edit this page on GitHub