Files
Nathan Rajlich e1e64e3de3 docs: apply Vercel technical writing standards (#3704)
* docs: apply Vercel technical writing standards

Audit the complete documentation corpus, package READMEs, skills, and
source TSDoc/comments against the vercel-technical-writing skill and
style-rules.md. Normalize sentence-case headings without changing
published anchors, remove prose em dashes and filler wording, improve
active voice and self-contained phrasing, standardize product/brand
capitalization, American English, list punctuation, units, and code
fence languages, and preserve exact runtime strings/table placeholders.

All executable code is unchanged. Modified skills have their metadata
versions bumped.

* docs: extend writing audit to repository Markdown

Apply the same technical-writing rules to design documents, compiler
specifications, workbench guides, package changelogs, and the remaining
tracked Markdown outside the deployed docs corpus. Preserve historical
meaning, commands, output literals, table placeholders, and heading
anchors.

* docs: exclude generated package changelogs from audit
2026-08-21 14:24:31 -07:00

319 lines
12 KiB
Plaintext

---
title: Postgres World
description: Production-ready, self-hosted world using PostgreSQL for storage and graphile-worker for job processing.
type: integration
summary: Deploy workflows to your own infrastructure using PostgreSQL and graphile-worker.
prerequisites:
- /docs/deploying
related:
- /worlds/local
- /worlds/vercel
---
The Postgres World is a production-ready backend for self-hosted deployments. It uses PostgreSQL for durable storage and [graphile-worker](https://github.com/graphile/worker) for reliable job processing.
Use the Postgres World to deploy workflows on your own infrastructure outside Vercel, such as a Docker container, Kubernetes cluster, or any cloud that supports long-running servers.
## Installation
Install the Postgres World package in your workflow project:
```package-install
@workflow/world-postgres
```
<Callout type="info">
Keep `workflow` and `@workflow/world-postgres` on the same major version and release
cycle. If your app uses a prerelease Workflow version, install the matching prerelease Postgres
World package. Mismatched versions fail before starting a run with an error that names the
spec versions the runtime supports and the one the World declares.
</Callout>
Configure the required environment variables to use the world and point it to your PostgreSQL database:
```bash title=".env"
WORKFLOW_TARGET_WORLD="@workflow/world-postgres"
WORKFLOW_POSTGRES_URL="postgres://user:password@host:5432/database"
```
Run the migration script to create the necessary tables in your database. Ensure `WORKFLOW_POSTGRES_URL` or `DATABASE_URL` is set when running this command:
<Tabs items={["npm", "pnpm", "Yarn", "Bun"]}>
<Tab value="npm">
```bash
npx --package=@workflow/world-postgres bootstrap
```
</Tab>
<Tab value="pnpm">
```bash
pnpm dlx --package @workflow/world-postgres bootstrap
```
</Tab>
<Tab value="Yarn">
```bash
yarn dlx --package @workflow/world-postgres bootstrap
```
</Tab>
<Tab value="Bun">
```bash
bunx --package @workflow/world-postgres bootstrap
```
</Tab>
</Tabs>
<Callout type="info">
The migration is idempotent and can safely be run as a post-deployment lifecycle script.
</Callout>
## Starting the World
To subscribe to the graphile-worker queue, your workflow app needs to start the world on server start. Here are examples for a few frameworks:
<Callout type="warn">
This step is specific to worlds that run a background worker, such as Postgres
World. Worlds whose queue delivers work over HTTP, including the Vercel World,
have no worker to subscribe, so `start()` does nothing there while the import
still adds overhead. Pulling in `workflow/runtime` from a server-startup hook puts
the whole runtime into the cold-start path before the first request is served.
</Callout>
<Tabs items={["Next.js", "SvelteKit", "Nitro"]}>
<Tab value="Next.js">
Create an `instrumentation.ts` file in your project root:
```ts title="instrumentation.ts" lineNumbers
export async function register() {
if (process.env.NEXT_RUNTIME !== "edge") {
const { getWorld } = await import("workflow/runtime");
const world = await getWorld();
await world.start?.();
}
}
```
<Callout type="info">
Learn more about [Next.js Instrumentation](https://nextjs.org/docs/app/guides/instrumentation).
</Callout>
</Tab>
<Tab value="SvelteKit">
Create a `src/hooks.server.ts` file:
```ts title="src/hooks.server.ts" lineNumbers
import type { ServerInit } from "@sveltejs/kit";
export const init: ServerInit = async () => {
const { getWorld } = await import("workflow/runtime");
const world = await getWorld();
await world.start?.();
};
```
<Callout type="info">
Learn more about [SvelteKit Hooks](https://svelte.dev/docs/kit/hooks).
</Callout>
</Tab>
<Tab value="Nitro">
Create a plugin to start the world on server initialization:
```ts title="plugins/start-pg-world.ts" lineNumbers
import { defineNitroPlugin } from "nitro/~internal/runtime/plugin";
export default defineNitroPlugin(async () => {
const { getWorld } = await import("workflow/runtime");
const world = await getWorld();
await world.start?.();
});
```
Register the plugin in your config:
```ts title="nitro.config.ts"
import { defineNitroConfig } from "nitropack";
export default defineNitroConfig({
modules: ["workflow/nitro"],
plugins: ["plugins/start-pg-world.ts"],
});
```
<Callout type="info">
Learn more about [Nitro Plugins](https://v3.nitro.build/docs/plugins).
</Callout>
</Tab>
</Tabs>
<Callout type="info">
The Postgres World requires a long-lived worker process that polls the database for jobs. This does not work on serverless environments. For Vercel deployments, use the [Vercel World](/worlds/vercel) instead.
</Callout>
## Observability
Use the Workflow CLI to inspect workflows stored in PostgreSQL:
```bash
# Set your database URL
export WORKFLOW_POSTGRES_URL="postgres://user:password@host:5432/database"
# List workflow runs
npx workflow inspect runs --backend @workflow/world-postgres
# Launch the web UI
npx workflow web --backend @workflow/world-postgres
```
If `WORKFLOW_POSTGRES_URL` is not set, the CLI defaults to `postgres://world:world@localhost:5432/world`.
Learn more in the [Observability](/docs/observability) documentation.
## Testing & compatibility
<WorldTestingPerformance worldId="postgres" />
## Configuration
You can set all configuration options through environment variables or programmatically through `createWorld()`.
### `WORKFLOW_POSTGRES_URL`
PostgreSQL connection string used by the runtime World.
Precedence: `WORKFLOW_POSTGRES_URL` > `DATABASE_URL` > `postgres://world:world@localhost:5432/world`
The `bootstrap` migration command uses the same precedence.
### `WORKFLOW_POSTGRES_JOB_PREFIX`
Prefix for graphile-worker queue job names. Useful when sharing a database between multiple applications. Default: `workflow_`
### `WORKFLOW_POSTGRES_WORKER_CONCURRENCY`
Number of concurrent workers polling for jobs. Default: `50`.
This value also bounds how many parent→child workflow polls can be in flight simultaneously. Every `await childRun.returnValue` inside a workflow holds a worker slot until the child run terminates. If you expect recursive or highly-fanned-out parent/child workflows, raise this ceiling above the peak number of concurrent polls. With the default of 50, the included `fibonacciWorkflow` end-to-end test (`fib(6)`, about 24 concurrent polls at peak) passes; deeper recursion or larger fanouts need a correspondingly larger setting.
### `WORKFLOW_POSTGRES_MAX_POOL_SIZE`
Maximum size of the internal `pg.Pool` used when `createWorld()` constructs the pool. Default: the `pg` default (`10`).
For higher worker concurrency, Graphile Worker recommends setting `maxPoolSize` to `10` or `queueConcurrency + 2`, whichever is larger.
### `WORKFLOW_POSTGRES_APPLICATION_MANAGED_SHUTDOWN`
Set to `1` when the application or framework coordinates shutdown and awaits `world.close()` before closing its workflow HTTP server and any caller-owned pool. Default: unset (`false`).
### `WORKFLOW_POSTGRES_RUN_STATUS_POLL_INTERVAL_MS`
How often a wait for a terminal run status re-reads the run, in milliseconds. Default: `1000`.
`await run.returnValue` asks the World to wait for the run to finish, and the Postgres World parks that wait on a `NOTIFY` issued by the run-terminal write. This interval is only the backstop: it bounds how long a lost notification can go unnoticed, so lowering it costs a query per waiting run per interval and buys nothing while notifications are arriving.
### `WORKFLOW_POSTGRES_HOOK_RETENTION_LIMIT_DAYS`
Maximum [`experimental_minRetention`](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) accepted by the Postgres World, in days. Default: `30`. Set this to the same limit as your production World so oversized values fail during development.
### `WORKFLOW_QUEUE_NAMESPACE`
Queue topic namespace shared by build output and the Postgres World. Default: unset.
For example, `custom` changes the queue topic prefix from `__wkf_workflow_` to `__custom_wkf_workflow_`. The value must be lowercase alphanumeric and start with a letter.
### `WORKFLOW_STREAM_FLUSH_INTERVAL_MS`
Group-commit window, in milliseconds, for the leading chunk of an idle stream. Default: `0` (dispatch immediately). A positive value holds the first chunk up to that long to collect a group, trading first-chunk latency for fewer requests. Chunks arriving while a request is in flight always coalesce into the next group regardless.
### Programmatic configuration
{/*@skip-typecheck: incomplete code sample*/}
```typescript title="my-world.ts" lineNumbers
import { createWorld } from "@workflow/world-postgres";
export default createWorld({
connectionString:
process.env.WORKFLOW_POSTGRES_URL ?? process.env.DATABASE_URL!,
jobPrefix: "myapp_",
namespace: "myapp",
queueConcurrency: 50,
maxPoolSize: 52, // overrides WORKFLOW_POSTGRES_MAX_POOL_SIZE
streamFlushIntervalMs: 10,
});
```
Options passed to `createWorld()` take precedence over the environment variables above. Export the World from a module and point `WORKFLOW_TARGET_WORLD` at that module:
You can also pass an existing `pg.Pool` as `pool` instead of a connection string.
```bash title=".env"
WORKFLOW_TARGET_WORLD="./my-world.ts"
```
### Application-managed shutdown
Graphile Worker responds automatically when the application is asked to shut down. If your application or framework already coordinates a broader shutdown sequence, set `WORKFLOW_POSTGRES_APPLICATION_MANAGED_SHUTDOWN=1` when selecting the package directly with `WORKFLOW_TARGET_WORLD`, or set `applicationManagedShutdown: true` in a programmatic World. Use the application's normal shutdown hook. This prevents Graphile Worker's default handler from terminating the process as soon as its queue stops. The hook must handle cleanup errors and await resources in this order:
1. `world.close()`
2. The workflow HTTP server
3. Any caller-owned `pg.Pool`
Closing the world stops new queue claims and waits for active jobs. After Graphile Worker's grace period, a pending workflow HTTP request is aborted. Graphile Worker unlocks that same row through its normal failure handling. The already-claimed delivery consumes a Graphile attempt and is retried only if its attempt budget remains; a one-attempt or final-attempt job is not retried. The shutdown handler does not insert a successor row. Because a client abort does not prove the server handler stopped, workflow and step handlers must tolerate at-least-once execution.
## How it works
The Postgres World uses PostgreSQL as a durable backend:
- **Storage**: Workflow runs, events, steps, and hooks are stored in PostgreSQL tables.
- **Job queue**: [graphile-worker](https://github.com/graphile/worker) handles reliable job processing with retries.
- **Streaming**: PostgreSQL NOTIFY/LISTEN enables real-time event distribution.
This architecture ensures workflows survive application restarts with all state reliably persisted. For implementation details, see the [source code](https://github.com/vercel/workflow/tree/main/packages/world-postgres).
## Deployment
Deploy your application to any cloud that supports long-running servers:
- Docker containers
- Kubernetes clusters
- Virtual machines
- Platform-as-a-service (PaaS) providers, such as Railway, Render, and Fly.io
Ensure your deployment has:
- Network access to your PostgreSQL database
- Environment variables configured correctly
- The `start()` function called on server initialization
<Callout type="info">
The Postgres World is not compatible with Vercel deployments. On Vercel, workflows automatically use the [Vercel World](/worlds/vercel) with zero configuration.
</Callout>
## Limitations
- **Requires long-running process**: Must call `start()` on server initialization; not compatible with serverless platforms
- **PostgreSQL infrastructure**: Requires a PostgreSQL database (self-hosted or managed)
- **Not compatible with Vercel**: Use the [Vercel World](/worlds/vercel) for Vercel deployments
For local development, use the [Local World](/worlds/local) which requires no external services.