[docs] Document Postgres World auth/security limitations explicitly - #3908
[docs] Document Postgres World auth/security limitations explicitly#3908pranaygp wants to merge 2 commits into
Conversation
The Postgres World was described as "production-ready" in the docs, the worlds manifest, and the building-a-world guide, while its README mentioned "reference implementation" only in passing and neither said anything about authentication. It inherits its queue HTTP handler from world-local, which validates the x-vqs-* header shape, the queue-name prefix, and the payload schema — never the caller — so any client that can reach POST /.well-known/workflow/v1/flow can forge or replay workflow and step invocations. Make the reference-implementation framing and the bring-your-own-auth expectation explicit instead: - Add a Security section to the package README, HOW_IT_WORKS, and both the v4 and v5 Postgres World docs pages: what is unauthenticated, how to gate it at the network edge, why framework middleware is the wrong layer, and that the World never presents a credential of its own. - Note that the World does not implement getEncryptionKeyForRun(), so data is stored unencrypted, and that self-hosted @workflow/web has no auth. - Drop "production-ready" from the docs pages, the worlds manifest, and the building-a-world reference callout, and add "no built-in authentication" and "no encryption" to the Limitations list. - Stop the Local World pages from pointing at the Postgres World for production without mentioning that it shares the same unauthenticated handler, and warn about self-hosted World security in the Deploying guide and the v5 Worlds configuration reference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
🦋 Changeset detectedLatest commit: 08de155 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
|
||
| ### Using a third-party World | ||
| <Callout type="warn"> |
There was a problem hiding this comment.
Not needed in this section. Let's leave it in the specific postgres world pages and leave this top level page to just be a short link section
There was a problem hiding this comment.
Reverted — this page is back to just the link section.
|
|
||
| ## Postgres World | ||
|
|
||
| The Postgres World is a self-hosted durable backend for long-running server processes. | ||
| The Postgres World is a self-hosted durable backend for long-running server processes. It is a reference implementation: none of the options below configure authentication, because the World does not authenticate Workflow's internal HTTP routes at all. Restrict them yourself — see [Postgres World security](/worlds/postgres#security). |
There was a problem hiding this comment.
Not needed in this section. This is just general configuration. leave it as is
There was a problem hiding this comment.
Reverted — left as is.
| @@ -68,7 +68,18 @@ For self-hosting or deploying to other cloud providers, you can use community-ma | |||
| </Card> | |||
| </Cards> | |||
|
|
|||
| ### Using a third-party World | |||
| <Callout type="warn"> | |||
There was a problem hiding this comment.
like in v4, it' not needed in this section. Let's leave it in the specific postgres world pages and leave this top level page to just be a short link section
There was a problem hiding this comment.
Reverted here too.
| </Callout> | ||
|
|
||
| <Callout type="info"> | ||
| **Reference implementation:** The [Postgres World source code](https://github.com/vercel/workflow/tree/main/packages/world-postgres) is a production-ready example of how to implement the World interface with a database backend and graphile-worker for queuing. | ||
| **Reference Implementation:** The [Postgres World source code](https://github.com/vercel/workflow/tree/main/packages/world-postgres) is a complete example of how to implement the World interface with a database backend and graphile-worker for queuing. Note that it deliberately leaves authentication to the deployment — see [Postgres World security](/worlds/postgres#security) for what a self-hosted World has to gate itself. |
There was a problem hiding this comment.
| **Reference Implementation:** The [Postgres World source code](https://github.com/vercel/workflow/tree/main/packages/world-postgres) is a complete example of how to implement the World interface with a database backend and graphile-worker for queuing. Note that it deliberately leaves authentication to the deployment — see [Postgres World security](/worlds/postgres#security) for what a self-hosted World has to gate itself. | |
| **Reference Implementation:** The [Postgres World source code](https://github.com/vercel/workflow/tree/main/packages/world-postgres) is a complete example of how to implement the World interface with a database backend and graphile-worker for queuing. It is not optimized for scale, speed, or security and should be used as a reference to full in those gaps in a custom World. Please see the [Postgres World docs](/worlds/postgres) for what a self-hosted World has to gate itself. |
There was a problem hiding this comment.
Applied to both v4 and v5. One tweak: the suggestion had "reference to full in those gaps", which I took as "fill in those gaps".
| @@ -81,4 +81,4 @@ The local world is designed for development, not production: | |||
| - **Single instance** - Cannot handle distributed deployments | |||
| - **No authentication** - Suitable only for local development | |||
|
|
|||
| For production deployments, use the [Vercel World](/worlds/vercel) or [Postgres World](/worlds/postgres). | |||
| For production deployments, use the [Vercel World](/worlds/vercel), which authenticates workflow traffic automatically, or the [Postgres World](/worlds/postgres) — which inherits this queue handler and its lack of authentication, so you must restrict its workflow routes yourself. See [Postgres World security](/worlds/postgres#security). | |||
There was a problem hiding this comment.
| For production deployments, use the [Vercel World](/worlds/vercel), which authenticates workflow traffic automatically, or the [Postgres World](/worlds/postgres) — which inherits this queue handler and its lack of authentication, so you must restrict its workflow routes yourself. See [Postgres World security](/worlds/postgres#security). | |
| For production deployments, use the [Vercel World](/worlds/vercel), which handles execution, persistence, multi-tenancy, scale, observability, and security for you on Vercel, or check out the [Postgres World](/worlds/postgres) - a tested reference implementation that implements the complete World spec and can be used to deploy workflows anywhere. |
There was a problem hiding this comment.
Applied verbatim to both v4 and v5.
| @@ -1,6 +1,6 @@ | |||
| --- | |||
| title: Postgres World | |||
| description: Production-ready, self-hosted world using PostgreSQL for storage and graphile-worker for job processing. | |||
| description: Self-hosted reference world using PostgreSQL for storage and graphile-worker for job processing. Bring your own authentication. | |||
There was a problem hiding this comment.
| description: Self-hosted reference world using PostgreSQL for storage and graphile-worker for job processing. Bring your own authentication. | |
| description: Self-hosted reference world using PostgreSQL for storage and graphile-worker for job processing. |
There was a problem hiding this comment.
Done — also dropped the same trailing sentence from the worlds-manifest.json description.
|
|
||
| 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). | ||
|
|
||
| ## Security | ||
|
|
||
| Unlike the [Vercel World](/worlds/vercel), which authenticates workflow traffic with OIDC tokens automatically, the Postgres World does **not** authenticate the requests that drive workflow execution. Adding that is your responsibility, and it needs to be in place before an app using this World is reachable by untrusted clients. |
There was a problem hiding this comment.
| Unlike the [Vercel World](/worlds/vercel), which authenticates workflow traffic with OIDC tokens automatically, the Postgres World does **not** authenticate the requests that drive workflow execution. Adding that is your responsibility, and it needs to be in place before an app using this World is reachable by untrusted clients. | |
| Unlike the [Vercel World](/worlds/vercel), which authenticates and [encrypts](/docs/how-it-works/encryption) workflow traffic automatically, the Postgres World does **not** automatically authenticate requests that drive workflow execution. Adding that is your responsibility and should be in place before an app using this World is used in production and reachable by untrusted clients. We also recommend [enabling encryption](https://workflow-sdk.dev/docs/how-it-works/encryption#custom-world-implementations). |
There was a problem hiding this comment.
Applied to both. One change: I used the relative /docs/how-it-works/encryption#custom-world-implementations instead of the absolute workflow-sdk.dev URL, so the v5 copy links to the v5 page rather than v4. Anchor verified against ## Custom World Implementations in both versions.
| That route runs workflow orchestration and queued step invocations (on adapters that mount a separate step route, `POST /.well-known/workflow/v1/step` behaves the same way). The handler validates only: | ||
|
|
||
| - The presence and shape of the `x-vqs-queue-name`, `x-vqs-message-id`, and `x-vqs-message-attempt` headers | ||
| - That the queue name carries the expected `__wkf_workflow_` prefix | ||
| - That the body matches the queue payload schema | ||
|
|
||
| There is no request signature, no shared secret, and no caller-identity check. Any client that can reach the route can submit a well-formed message of its own, or re-send a body it captured earlier, and the runtime will execute it — starting or advancing runs and invoking steps directly, including steps your application would normally only reach after its own gating (an approval, a payment check, a permission test). Because nothing in a message is bound to a caller, a replayed delivery is indistinguishable from a genuine one. | ||
|
|
||
| Two related routes have their own limits worth knowing: | ||
|
|
||
| - `POST /.well-known/workflow/v1/webhook/:token`, created by [`createWebhook()`](/docs/api-reference/workflow/create-webhook), is authorized by the token in the URL and nothing else. When you need more than that, use [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own authenticated route and call [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) once you have authorized the request. | ||
| - `GET /.well-known/workflow/v1/manifest.json` responds with `404` unless the `WORKFLOW_PUBLIC_MANIFEST` environment variable is set to `1`. Leave it unset outside of testing — the manifest lists your workflow and step names. |
There was a problem hiding this comment.
Too verbose. It's enough to say that this route is publicly accessible on the internet by default, and should be protected. And then just mention the 2 other routes (webhook and manifest and the exceptions there)
There was a problem hiding this comment.
Cut. The section is now "Protect the queue route": one paragraph saying the flow route is publicly reachable by default and has no signature/secret/caller check, then the two exceptions (webhook token, manifest 404). The header-validation bullet list and the replay paragraph are gone. Applied the same trim to the README.
| - **Not compatible with Vercel**: Use the [Vercel World](/worlds/vercel) for Vercel deployments | ||
| - **Reference implementation** - Durable and complete, but not tuned for the performance or scale properties of a managed backend; a production deployment typically runs workers in separate processes with a more robust queuing system | ||
| - **No built-in authentication** - The workflow HTTP routes accept any request that reaches them; you must restrict them yourself (see [Security](#security)) | ||
| - **No encryption** - Workflow and step data is stored unencrypted; the World does not implement [end-to-end encryption](/docs/how-it-works/encryption) |
There was a problem hiding this comment.
| - **No encryption** - Workflow and step data is stored unencrypted; the World does not implement [end-to-end encryption](/docs/how-it-works/encryption) | |
| - **No encryption** - Workflow and step data is stored unencrypted; the World does not currently implement [end-to-end encryption](/docs/how-it-works/encryption) |
|
|
||
| <Callout type="info"> | ||
| If you have a World on the 4.x spec, see [Upgrading a World to v5](/worlds/upgrading-to-v5) for the interface and contract changes. | ||
| **Reference Implementation:** The [Postgres World source code](https://github.com/vercel/workflow/tree/main/packages/world-postgres) is a complete example of how to implement the World interface with a database backend and graphile-worker for queuing. Note that it deliberately leaves authentication to the deployment — see [Postgres World security](/worlds/postgres#security) for what a self-hosted World has to gate itself. |
There was a problem hiding this comment.
Applied — same text as v4, with the "fill in those gaps" reading.
|
|
||
| For production deployments, use the [Vercel World](/worlds/vercel) or [Postgres World](/worlds/postgres). | ||
| For production deployments, use the [Vercel World](/worlds/vercel), which authenticates workflow traffic automatically, or the [Postgres World](/worlds/postgres) — which inherits this queue handler and its lack of authentication, so you must restrict its workflow routes yourself. See [Postgres World security](/worlds/postgres#security). |
| @@ -1,6 +1,6 @@ | |||
| --- | |||
| title: Postgres World | |||
| description: Production-ready, self-hosted world using PostgreSQL for storage and graphile-worker for job processing. | |||
There was a problem hiding this comment.
see all the v4 comments for this world too
There was a problem hiding this comment.
All of them applied to v5: description, Security intro with the encryption recommendation, condensed route section, and "does not currently implement".
| > of one. A production deployment typically runs workers in separate processes | ||
| > with a more robust queuing system, and **must** put its own authentication in | ||
| > front of the workflow HTTP routes. Read [Security](#security) before you | ||
| > deploy it. |
There was a problem hiding this comment.
| > deploy it. | |
| > deploy it. | |
| > | |
| > While some customers have been successful in deploying the Postgres World as is, for production use-cases, we highly recommend cloning this reference implementation and adapting it to your persistence, network stack, scale and security requirements. |
There was a problem hiding this comment.
Added. Also reworked the intro callout on both docs pages to match this framing — "implements the complete World spec and is tested, but not optimized for scale, speed, or security" plus the clone-and-adapt recommendation — so the README and the docs say the same thing.
|
|
||
| ### Data at rest | ||
|
|
||
| Postgres World does not implement `getEncryptionKeyForRun()`, so it does not |
There was a problem hiding this comment.
very easy for a derived world to actually implement these for encryption
There was a problem hiding this comment.
Good point — reframed. It now says a derived World can opt in by implementing that single method, links to Custom World implementations, and calls that the recommended route for sensitive data. The docs pages say the same, and the Limitations bullet is now "does not currently implement".
|
|
||
| - Graphile jobs are acknowledged only after execution finishes, or after the worker durably schedules a delayed follow-up job | ||
| - Backlog stays in PostgreSQL when all execution slots are busy | ||
| - Retry and sleep-style delays use Graphile `runAt` scheduling | ||
| - Workflow orchestration and queued step execution are both sent through `/.well-known/workflow/v1/flow` | ||
| - That route is not authenticated by this World — see [Security](#security) |
| @@ -16,7 +16,7 @@ | |||
| "type": "official", | |||
| "package": "@workflow/world-postgres", | |||
| "name": "Postgres", | |||
| "description": "Production-ready, self-hosted world using PostgreSQL for durable storage and graphile-worker for reliable job processing.", | |||
| "description": "Self-hosted reference world using PostgreSQL for durable storage and graphile-worker for reliable job processing. Bring your own authentication.", | |||
There was a problem hiding this comment.
| "description": "Self-hosted reference world using PostgreSQL for durable storage and graphile-worker for reliable job processing. Bring your own authentication.", | |
| "description": "Self-hosted reference world using PostgreSQL for durable storage and graphile-worker for reliable job processing." |
- Revert the additions to deploying.mdx (v4 + v5) and the v5 Worlds configuration reference: those pages stay a short link section and general configuration, with the security detail living on the Postgres World pages. - Apply the suggested wording for building-a-world, local.mdx, the Postgres frontmatter description, the Security intro (now also recommending encryption), the "does not currently implement" limitation, and the worlds manifest description. - Condense "What is unauthenticated" into "Protect the queue route": the flow route is publicly reachable by default and must be protected, plus the two exceptions (webhook token, manifest 404). - README: add the clone-and-adapt recommendation to the notice, note that a derived World can opt into encryption by implementing getEncryptionKeyForRun(), and drop the redundant Queue Behavior bullet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
|
@NathanColosimo could you take a look and merge if it looks good? All 16 of @pranaygp's comments are addressed and answered inline — summary in the "Review round 2" section of the description. Docs only, no behavior change. |
Why
Someone pointed out that the generated internal queue endpoints accept forged, unauthenticated requests when using the Postgres World. That's true, and it's the intended trust model for a reference implementation — but our docs didn't say so anywhere:
docs/content/worlds/{v4,v5}/postgres.mdx,worlds-manifest.json, and the building-a-world guide all called it "production-ready".Per the thread: we can't prescribe how people do auth, so the outcome is to be explicit about the limitation, the way
@workflow/webalready is about bringing your own auth.Docs only — no behavior change.
What is actually unauthenticated
packages/world-postgres/src/queue.tstakescreateQueueHandlerstraight from@workflow/world-local, and that handler (packages/world-local/src/queue.ts:373) validates only thex-vqs-*header shape, the__wkf_workflow_queue-name prefix, and the payload schema. No signature, no shared secret, no caller identity. So anything that can reachPOST /.well-known/workflow/v1/flowcan start/advance runs and invoke steps directly, including a replay of a previously captured body.Changes
Package docs
packages/world-postgres/README.md— reference-implementation notice at the top (including the recommendation to clone and adapt for production), plus a## Securitysection: protect the queue route, bring your own auth, and data at rest.packages/world-postgres/HOW_IT_WORKS.md— one-line note on the trust assumption where HTTP delivery is described.Docs site (v4 + v5)
worlds/*/postgres.mdx— dropped "production-ready" from the frontmatter and intro, added awarncallout, a## Securitysection, a deployment-checklist item, and "reference implementation" / "no built-in authentication" / "no encryption" to Limitations.worlds/*/local.mdx— reworded the "for production deployments" pointer.worlds/*/building-a-world.mdx— the reference-implementation callout no longer claims production-ready.worlds-manifest.json— description no longer claims production-ready.The auth guidance is deliberately network-level (ingress / reverse proxy / firewall, mTLS or a shared-secret header when the routes cross hosts) rather than framework middleware, because:
/.well-known/workflow/*from the proxy matcher, and a handler that consumes the internal body breaks execution; andAlso documented, since they came up while tracing the trust boundary: the
webhook/:tokenroute is authorized by the token alone,manifest.jsonis 404 unlessWORKFLOW_PUBLIC_MANIFEST=1, and the World does not currently implementgetEncryptionKeyForRun()— with a pointer to Custom World implementations, since a derived World can opt into encryption with that one method.Review round 2
All 16 comments from @pranaygp are addressed and replied to inline:
docs/*/deploying.mdx(v4 + v5) anddocs/v5/configuration/worlds.mdx— those pages stay a short link section and general configuration.getEncryptionKeyForRun().Two small deviations, both flagged in the threads: the building-a-world suggestion's "reference to full in those gaps" is applied as "fill in those gaps", and the encryption link uses the relative
/docs/...form so the v5 copy links to v5 rather than to the v4 page.Docs Preview
Base URL from the
vercel[bot]comment (workflow-docsrow): https://workflow-docs-git-document-pg-world-auth.vercel.sh — links require Vercel team access.🤖 Generated with Claude Code