Operate an OpenShop app
Use this guide after deploying the app. You need access to the embedded admin and to the web and worker service logs. For the process model, read Architecture.
1. Start separate processes
Section titled “1. Start separate processes”Web service:
pnpm exec openshop startWorker service:
pnpm exec openshop worker --concurrency=5Do not place the commands sequentially in one shell script: openshop start is
long-running, so the worker line would never execute. Configure two services,
containers, process-manager entries, or terminal sessions.
2. Check health and run a smoke flow
Section titled “2. Check health and run a smoke flow”The unauthenticated health endpoint confirms only that the HTTP process responds:
curl --fail --silent https://your-app.example.com/healthExpected shape:
{ "status": "ok", "timestamp": "2026-07-29T12:00:00.000Z"}It does not prove PostgreSQL, Shopify, or workers are healthy. Complete a smoke
check by dispatching a harmless flow from the embedded admin UI and watching it
move from pending to completed.
3. Monitor runs
Section titled “3. Monitor runs”In the embedded admin UI:
- Filter runs by
pending,running,sleeping, orfailed. - Open a run to inspect step status and structured logs.
- Select Retry, then Resume on a finished run to keep completed steps.
- Use Restart (the API’s
resetmode) only when completed steps are safe to execute again.
Operational signals:
| Signal | Likely cause | Action |
|---|---|---|
Growing pending count |
No worker, saturated concurrency, or DB failure | Check worker processes and database connectivity; then scale workers. |
Old running runs |
Worker crash or a long step | Check worker logs and step timeout; expired leases allow another worker to reclaim work. |
Repeated sleeping runs |
Retrying failures or explicit step.sleep |
Inspect run logs and availableAt. |
Many failed runs |
Provider, Shopify, validation, or deployment regression | Filter logs by error and compare the first failure time with deployments. |
| Cron absent | Bad schedule, disabled per-shop override, or web process down | Check config, cron toggle, and web/scheduler process. |
4. Scale workers
Section titled “4. Scale workers”Start additional identical worker processes against the same database. Workers use
PostgreSQL row locking with SKIP LOCKED, so separate processes can safely claim
different runs.
Configure defaults in openshop.config.ts:
import { app } from '#app'import { syncOrders } from '#flows/syncOrders'
export default app.defineConfig({ flows: { syncOrders }, worker: { concurrency: 5, pollIntervalMs: 1_000, pollMaxIntervalMs: 5_000, pollBackoffCoefficient: 1.5, leaseDurationMs: 30_000, },})A CLI --concurrency override wins for that worker process. The complete
worker defaults are in the reference.
Scale gradually. Total simultaneous flow runs are approximately:
worker process count × concurrency per workerAlso budget PostgreSQL connections per process using PGPOOL_MAX.
Recover failed runs
Section titled “Recover failed runs”- Open the failed run and find the first failed step and its error.
- Correct the cause, such as invalid provider credentials or missing Shopify scopes.
- Select Retry, then Resume to reuse completed checkpoints.
- Verify the resumed run completes and check the downstream result.
Choose Restart only if every side effect can safely happen again. Read Checkpoints and retries before resetting a run that writes to another system.
Cancel, retry, and delete safely
Section titled “Cancel, retry, and delete safely”- Cancel requests mark an active run
canceledand signal the process currently executing it. - Long-running libraries stop promptly only when they receive
ctx.signal. - Resume retry keeps completed steps; reset retry discards all step results.
- Active runs must be canceled before deletion.
- Bulk delete accepts at most 100 IDs and skips active runs.
Pass the abort signal to compatible APIs:
import { app } from '#app'
export const refreshCatalog = app.defineFlow({ name: 'refreshCatalog', async run({ signal, step }) { await step('download catalog', async () => { const response = await fetch('https://catalog.example.com/export', { signal }) if (!response.ok) throw new Error(`Catalog returned ${response.status}`) return response.text() }) },})Graceful deployment
Section titled “Graceful deployment”- Stop routing new HTTP traffic to the old web process.
- Send
SIGTERMto old workers. - Allow at least
leaseDurationMsfor active work to finish. - Apply committed database migrations.
- Start the new web process and workers from the same build.
- Dispatch one harmless flow and confirm completion.
The worker stops claiming new runs on shutdown and waits up to its lease duration for active runs. If a process dies, its leases eventually expire and another worker can reclaim the runs.
Backups and incident data
Section titled “Backups and incident data”Back up PostgreSQL using your managed database policy. OpenShop state includes installations, encrypted access tokens, provider configurations, flow runs, steps, logs, cron overrides, and MCP tokens/audits.
During an incident, preserve:
- application version and deployment time;
- web and worker process logs;
- affected run IDs and exported run logs;
- database health and connection saturation;
- recent provider, Shopify scope, encryption-key, or environment changes.
Never publish exported logs without checking their structured payloads for customer or order data.
For log queries and exports, see Logging. For API status codes and retry endpoints, see Admin API.