fix(traffic): never sync DATABASE_URL onto a Worker #541
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gmackie/ForgeGraph!541
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/canary-never-push-database-url"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What broke
syncSecretsToCanarypushed every stage secret to the canary Worker. The production store legitimately holdsDATABASE_URLfor consumers that are not Workers (migrations, systemd apps, local dev), so the canary received it too.apps/web/src/lib/db.tsresolves the connection withDATABASE_URLtaking precedence over theHYPERDRIVEbinding — and the comment directly above that code says why it must never be set on a Worker. So the sync silently repointed the Worker at a Tailscale address the Cloudflare edge cannot reach.Observed, 2026-08-30
A sync put
DATABASE_URLon the Worker serving the control-plane apex:/api/fg/nodes/api/fg/apps/api/fg/hub-tokenThe Hyperdrive binding was present and intact the whole time, just unused. That is why it reads as a database outage instead of a secret-sync bug. Deleting the one secret restored all three to 200 immediately.
syncSecretsToCanaryruns on every traffic shift, not only at enable, so it re-broke production each time it ran. This is the same mechanism as #452.Fix
Withhold the DB-routing keys at the Worker boundary — that is where the constraint lives. Removing them from the store would break the non-Worker consumers that need them.
WORKER_FORBIDDEN_SECRET_KEYS=DATABASE_URL,DATABASE_URL_LOCAL,DATABASE_OWNER_PASSWORDskipped, notfailed. A correct sync must not look broken, and conflating the two would mask real push errors. Both callers read only.failed, so the added field is backward compatible.missingCanarySecretsfilters the same set: the primary predates the denylist and still carries these, so counting them as missing would report a gap no sync can ever close — and under enforcement that blocks every split forever.Tests
23 pass. Three new:
Two existing tests used
DATABASE_URLas an arbitrary placeholder, one of them asserting it is pushed — that assertion encoded the bug. Swapped toSENTRY_DSNso they still cover push-all and failure-reporting.Note:
packages/apivitest cannot run locally through the normal entrypoint (PGlite global setup fails, pre-existing and unrelated); these were run with a minimal config scoped to this file.🤖 Generated with Claude Code