You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(worker): unblock deploys by removing the unprovisioned D1 binding
`wrangler.toml` declared a D1 binding whose `database_id` was a placeholder
of all zeroes for a database that was never created. Cloudflare validates
bindings when the script is uploaded, so every `wrangler deploy` failed:
D1 binding 'RATINGS_DB' references database
'00000000-0000-0000-0000-000000000000' which was not found. [code: 10181]
That took the whole worker down with it, including the CORS data proxy,
which has nothing to do with D1. The worker has been undeployable since
#426 landed on 2026-06-13; production still serves pre-#426 code, and
`/v1alpha` falls through to the upstream origin as a 404. Critically,
`data/festivals.json` edits also trigger this deploy, so festival data
changes could not reach production either.
Comment the binding out rather than leave a placeholder id. `reviews.ts`
already degrades to 503 UNAVAILABLE / STORAGE_UNCONFIGURED when
`env.RATINGS_DB` is absent, so the review API keeps the behaviour it has
had all along (it has never served traffic) while the proxy, /health and
/festivals.json deploy again. Provisioning D1 stays a one-time maintainer
step, now documented as uncomment-and-paste.
Nothing caught this: the vitest pool and `wrangler dev` use a simulated
local D1 that ignores the id, and `wrangler deploy --dry-run` — CI's
`validate-worker` job — exits 0 on a dangling binding (verified against
wrangler 4.98.0). The PR that broke deploys had a green CI run. So:
- move the test D1 binding to `vitest.config.js`, decoupling the suite
from the deployed binding set
- add `test/wrangler-config.test.js`, which fails on a placeholder id and
on a D1 binding reappearing without the degraded-mode tests being
updated (verified red against the exact config that broke production)
- add tests asserting every /v1alpha route returns 503
STORAGE_UNCONFIGURED and that /health and /festivals.json still work
with no storage — the configuration actually being deployed
Docs corrected across the worker README and seven skills, which described
the placeholder as deploy-neutral and named `--dry-run` as the check that
would catch this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7Rma9m9unUYXYgpon6Rph
Copy file name to clipboardExpand all lines: .claude/skills/build-and-env/SKILL.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -241,7 +241,7 @@ train.
241
241
|`TEST_LOG` / `ANALYZE_LOG`|`mktemp /tmp/test-XXXXXX.log` / `/tmp/analyze-XXXXXX.log` per run (`mise-tasks/test.sh:6`, `mise-tasks/analyze.sh:6`) | the `test`/`analyze` file-tasks themselves (tee output, preserve exit code via `${PIPESTATUS[0]}`) | Same on both; override to a stable path when you need to grep the same log across multiple invocations without re-running |
242
242
|`ENVIRONMENT` (wrangler `[vars]`) |`"production"` (`cloudflare-worker/wrangler.toml:6-7`) | Worker code (`worker.js`/`shared.ts`) for any environment-conditional behavior | Only one value committed — there's no separate staging `[vars]` block in `wrangler.toml`; staging behavior is driven by **origin-based** CORS/bucket logic instead (below), not this var |
243
243
|`RATINGS_BUCKET` (env override) | unset — falls back to `resolveBucket(origin, env)` (`cloudflare-worker/shared.ts:23-27`): `origin === "https://cambeerfestival.app"` → `"prod"`, else `"test"`|`shared.ts` bucket resolution for the D1 `reviews` table's composite key | Not set anywhere in committed config today; it's an escape hatch for forcing a bucket regardless of request origin. Do not set it in production without understanding it silently overrides the origin check |
244
-
|`RATINGS_DB` (D1 binding, not an env var) |`[[d1_databases]] binding = "RATINGS_DB"`, `database_id = "00000000-0000-0000-0000-000000000000"`**placeholder** (`wrangler.toml:20-25`) |`reviews.ts` via `env.RATINGS_DB`; missing/misconfigured → worker returns 503 `STORAGE_UNCONFIGURED`| Tests/local dev use wrangler's simulated local D1 (id ignored); a real deploy needs `wrangler d1 create cbf-myfestival` + paste the real id + `wrangler d1 migrations apply` — see `run-and-operate` for the provisioning runbook |
244
+
|`RATINGS_DB` (D1 binding, not an env var) |`[[d1_databases]]` block **commented out** in `wrangler.toml` (a placeholder `database_id` broke every deploy — error 10181); the test-only binding lives in `vitest.config.js` (`miniflare.d1Databases`) |`reviews.ts` via `env.RATINGS_DB`; missing/misconfigured → worker returns 503 `STORAGE_UNCONFIGURED`| Tests/local dev use a simulated local D1 (id ignored; `--dry-run` doesn't check resource existence either, so `test/wrangler-config.test.js` guards the placeholder case); enabling it needs `wrangler d1 create cbf-myfestival` + uncomment the block with the real id + `wrangler d1 migrations apply --remote` — see `run-and-operate` for the provisioning runbook |
245
245
|`BASE_URL` (Playwright) |`"http://127.0.0.1:8080"` (`playwright.config.ts:34`) |`test-e2e/*.spec.ts` via `page.goto`/`baseURL`| Local/CI default targets a locally-served build; CI's `smoke-test-preview` job sets `BASE_URL=<Cloudflare Pages preview URL>` to run `csp-smoke.spec.ts` against a real deployed CSP policy — the only place that check is meaningful (`web/_headers` CSP isn't exercised any other way) |
Copy file name to clipboardExpand all lines: .claude/skills/debugging-playbook/SKILL.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -92,7 +92,8 @@ most battles in this repo have already been fought once.
92
92
93
93
| Symptom | Likely cause | Discriminating experiment | Fix pattern | History |
94
94
|---|---|---|---|---|
95
-
|`/v1alpha/...` returns 503 with reason `STORAGE_UNCONFIGURED`|`env.RATINGS_DB` D1 binding missing — `cloudflare-worker/wrangler.toml` ships a placeholder `database_id = "00000000-..."`|`curl -s https://data.cambeerfestival.app/v1alpha/festivals/x/drinks/y/review -H 'X-Device-Id: t'` and check the error body (`reviews.ts:158-166`) | Provision D1: `wrangler d1 create cbf-myfestival` → paste real id into wrangler.toml → `wrangler d1 migrations apply cbf-myfestival --remote` (`--remote` migrates the real DB, not the local sim); token needs D1:Edit. Full runbook: skill `run-and-operate`| v1alpha arc (PR #426); deploy still pending |
95
+
|`/v1alpha/...` returns 503 with reason `STORAGE_UNCONFIGURED`| Expected, not a bug: `env.RATINGS_DB` is absent because the `[[d1_databases]]` block in `cloudflare-worker/wrangler.toml` is **deliberately commented out** until a database is provisioned |`curl -s https://data.cambeerfestival.app/v1alpha/festivals/x/drinks/y/review -H 'X-Device-Id: t'` and check the error body (`reviews.ts:158-166`) | Provision D1: `wrangler d1 create cbf-myfestival` → uncomment the block and paste the real id → `wrangler d1 migrations apply cbf-myfestival --remote` (`--remote` migrates the real DB, not the local sim); token needs D1:Edit. Full runbook: skill `run-and-operate`| v1alpha arc (PR #426); binding disabled to unblock deploys |
96
+
|`wrangler deploy` fails with `D1 binding 'X' references database '…' which was not found [code: 10181]`, while `npm test` is green | A binding names a resource that doesn't exist in the account. Cloudflare validates bindings at upload; the vitest pool and `wrangler dev` use simulated local resources and never check the id, so tests cannot catch it |**Not**`--dry-run` — it exits 0 on a dangling binding (verified, wrangler 4.98.0). Read the deploy log for the resource id, then confirm the resource exists in the account (dashboard, or `wrangler d1 list`). `cloudflare-worker/test/wrangler-config.test.js` catches the placeholder case offline | Either provision the resource, or remove/comment out the binding so the rest of the worker can deploy (the code already degrades to 503 `STORAGE_UNCONFIGURED`). Never leave a placeholder id in place: it blocks *every* deploy, proxy included | Broke all worker deploys 2026-06-13 → 2026-08-10; the live worker silently stayed on pre-#426 code for ~2 months |
96
97
| Browser: CORS error calling the worker; curl works fine | Origin not in the allow-list — the worker returns NO CORS headers for unknown origins (silent reject) | Compare your page's `Origin` against `ALLOWED_ORIGINS` (`cloudflare-worker/worker.js:29-37`) and the wildcard suffixes in `getCorsHeaders` (worker.js:279-329): `*.cambeerfestival.pages.dev`, `*.staging-cambeerfestival.pages.dev`, `*.trycloudflare.com`| Add the origin to the allow-list — but `cloudflare-worker/` is on the Do-Not-Modify list; needs explicit maintainer request + PR | Allow-list design; ops trap (trycloudflare wildcard live in prod) |
97
98
| Worker returns 502 | Upstream `data.cambridgebeerfestival.com` fetch failed — the worker proxies everything not matched by its own routes |`curl -si https://data.cambridgebeerfestival.com/<same-path>` — is upstream itself down/erroring? | Nothing to fix app-side; upstream CAMRA feeds are untouchable (unwritten rule). The app's SWR cache is the mitigation — verify cached data still renders with the refresh notice | worker.js:102-137 |
Copy file name to clipboardExpand all lines: .claude/skills/reference/SKILL.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -273,7 +273,7 @@ are at different maturity levels and it's easy to conflate them.
273
273
| Surface | Status | Where implemented | What it does |
274
274
|---|---|---|---|
275
275
|**Static beverage feeds**| Live in production, untouchable upstream data |`data.cambridgebeerfestival.com` (CAMRA's own static files) proxied through `cloudflare-worker/worker.js` at `data.cambeerfestival.app/{festivalId}/{category}.json`; also `/festivals.json` (embedded registry, `no-cache, must-revalidate`) and `/{festivalId}/available_beverage_types.json` (scrapes an upstream Apache directory listing, 1h cache) | The entire catalogue the app renders — Producers→Products, per festival per category (§3). This is what `BeerApiService`/`FestivalService` fetch. |
276
-
|**v1alpha Review API**| Live code, deployed worker, **D1 database not yet provisioned** (`wrangler.toml`ships a placeholder `database_id`) |`cloudflare-worker/reviews.ts` + `shared.ts`, routed at `/v1alpha/...` inside the same worker; D1 table `reviews` (`cloudflare-worker/migrations/0001_create_reviews_table.sql`) | Anonymous star-rating + "would recommend" reviews, keyed by `X-Device-Id` header (not signed-in identity yet — `user_id` column reserved for a future sign-in upgrade). GET/PATCH/DELETE one review, list caller's reviews, get/list aggregate summaries per drink. Any unmatched `/v1alpha/*` path 404s — it is never proxied upstream. |
276
+
|**v1alpha Review API**| Live code, deployed worker, **D1 database not yet provisioned**, so `/v1alpha` answers 503 `STORAGE_UNCONFIGURED` in production (the `[[d1_databases]]` block in `wrangler.toml`is commented out) |`cloudflare-worker/reviews.ts` + `shared.ts`, routed at `/v1alpha/...` inside the same worker; D1 table `reviews` (`cloudflare-worker/migrations/0001_create_reviews_table.sql`) | Anonymous star-rating + "would recommend" reviews, keyed by `X-Device-Id` header (not signed-in identity yet — `user_id` column reserved for a future sign-in upgrade). GET/PATCH/DELETE one review, list caller's reviews, get/list aggregate summaries per drink. Any unmatched `/v1alpha/*` path 404s — it is never proxied upstream. |
277
277
|**proto CatalogService / MyFestivalService**|**Paper contract only** — defines the intended future v1alpha REST surface via `google.api.http` annotations, generates OpenAPI, but has **no server implementation** in the worker (`/v1alpha` routing only wires up `handleReviews`, i.e. the Review API above; `CatalogService`'s `ListFestivals`/`GetFestival`/`ListDrinks` RPCs have no handler) |`proto/cambeerfestival/festival/v1alpha/{catalog_service,my_festival_service,drink_entry,drink_summary,festival,drink,producer}.proto` → `buf generate` → `docs/code/api/openapi/openapi.yaml` (Redoc-published by `api-docs.yml`) | Design-time contract for where the API is headed: a typed, resource-oriented catalogue API (AIP-compliant) and a richer `DrinkEntry`/`MyFestivalService` (favourite/rating/note/pour-count sync with soft-delete tombstones and etag concurrency) intended to eventually replace/extend the anonymous Review API. Full workflow and AIP facts: skill `api-contract`. |
278
278
279
279
The **static feeds** and the **v1alpha Review API** are the two surfaces that
0 commit comments