This Cloudflare Worker proxies requests to data.cambridgebeerfestival.com and adds CORS headers, allowing the Flutter web app to access the API data.
- The Flutter app makes requests to
https://cbf-data-proxy.<your-subdomain>.workers.dev/cbf2025/beer.json - The worker fetches data from
https://data.cambridgebeerfestival.com/cbf2025/beer.json - The worker adds CORS headers and returns the response to the app
- A Cloudflare account (free tier works)
CLOUDFLARE_API_TOKENsecret added to GitHub repository
- Go to Cloudflare Dashboard
- Navigate to My Profile → API Tokens
- Click Create Token
- Use the Edit Cloudflare Workers template, or create a custom token with:
- Account → Workers Scripts → Edit
- Zone → Workers Routes → Edit (if using custom domain)
- Copy the token and add it as
CLOUDFLARE_API_TOKENin GitHub repository secrets
If you want to deploy manually first:
cd cloudflare-worker
npm install
npx wrangler login
npx wrangler deployThe worker URL will be displayed after deployment (e.g., https://cbf-data-proxy.<account>.workers.dev).
The GitHub Actions workflow automatically deploys the worker on push to main. Ensure:
CLOUDFLARE_API_TOKENis set in repository secrets (Settings → Secrets and variables → Actions → New repository secret)
Troubleshooting: If you see "Unable to authenticate request [code: 10001]", your API token may be:
- Missing from GitHub secrets
- Expired or revoked
- Missing required permissions (needs "Workers Scripts: Edit" at minimum)
The worker provides several endpoints:
Proxies requests to data.cambridgebeerfestival.com with CORS headers:
/{festivalId}/{beverageType}.json- Get beverage data (e.g.,/cbf2025/beer.json)
Dynamic API endpoints that provide festival metadata:
/festivals.json- Returns the festivals registry with all festival metadata/{festivalId}/available_beverage_types.json- NEW! Dynamically discovers available beverage types for a festival
Example:
# Get available beverage types for CBF 2025
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/cbf2025/available_beverage_types.json
# Response:
{
"festival_id": "cbf2025",
"available_beverage_types": [
"apple-juice",
"beer",
"cider",
"international-beer",
"low-no",
"mead",
"perry",
"wine"
],
"timestamp": "2025-12-02T10:30:00.000Z"
}This endpoint:
- Dynamically fetches the directory listing from the upstream API
- Parses the HTML to find all
.jsonfiles - Returns them as a sorted array
- Caches the result for 1 hour
Aggregate drink ratings and "would recommend" answers, backed by D1 (SQLite).
The first step towards an online "my festival". The API is resource-oriented
following Google's AIPs — the contract is defined in
proto/ and an OpenAPI spec is generated from it (see proto/README.md).
Writes are local-first on the client; the server holds the shared aggregate.
Every row and query is scoped by a bucket (test or prod, derived from the
request origin; only https://cambeerfestival.app → prod) so test traffic
never mixes with production data. A RATINGS_BUCKET worker var can pin it.
Resources (the device is the record id, so a device has one record per drink):
| Method | Path | Purpose |
|---|---|---|
PATCH |
/v1/festivals/{f}/drinks/{d}/ratings/{device} |
Upsert a rating ({value:1-5}) |
GET |
/v1/festivals/{f}/drinks/{d}/ratings/{device} |
Get a device's rating |
DELETE |
/v1/festivals/{f}/drinks/{d}/ratings/{device} |
Remove a device's rating |
GET |
/v1/festivals/{f}/ratingSummaries/{d} |
Aggregate for one drink |
GET |
/v1/festivals/{f}/ratingSummaries?page_size=&page_token= |
Paginated list of aggregates |
The recommendations / recommendationSummaries collections mirror this with
a {wouldRecommend: bool} body. PATCH is an upsert (AIP-134 allow_missing);
DELETE takes the id in the path with no body (AIP-135) and is NOT_FOUND when
absent. Errors use the structured google.rpc.Status shape (AIP-193).
# Upsert a rating, get back the Rating resource
curl -X PATCH https://data.cambeerfestival.app/v1/festivals/cbf2025/drinks/beer-1/ratings/dev-1 \
-H 'Content-Type: application/json' -d '{"value":4}'
# -> {"name":"festivals/cbf2025/drinks/beer-1/ratings/dev-1","value":4,"updateTime":"2026-06-12T20:00:00.000Z"}
# Aggregate for one drink
curl https://data.cambeerfestival.app/v1/festivals/cbf2025/ratingSummaries/beer-1
# -> {"name":"festivals/cbf2025/ratingSummaries/beer-1","ratingCount":3,"averageRating":4.0}
# % would recommend for one drink
curl https://data.cambeerfestival.app/v1/festivals/cbf2025/recommendationSummaries/beer-1
# -> {"name":"...","responseCount":2,"recommendCount":1,"recommendRate":0.5}The database_id in wrangler.toml is a placeholder. Local wrangler dev and
the vitest test pool use a simulated local D1 and ignore it, so the full test
suite runs with no real database. Before deploying:
cd cloudflare-worker
wrangler d1 create cbf-ratings # prints the database_id
# paste the id into wrangler.toml ([[d1_databases]].database_id)
wrangler d1 migrations apply cbf-ratings # applies migrations/*.sqlThe deploy CLOUDFLARE_API_TOKEN must include D1: Edit in addition to
Workers Scripts: Edit. To wipe test data: DELETE FROM ratings WHERE bucket='test'.
/health- Returns{"status": "ok"}for monitoring
After deployment, test the proxy:
# Test beverage data proxy
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/cbf2025/beer.json
# Test dynamic beverage types discovery
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/cbf2025/available_beverage_types.json
# Test festivals registry
curl https://cbf-data-proxy.<your-subdomain>.workers.dev/festivals.jsonOnce deployed, update lib/models/festival.dart to use the proxy URL:
dataBaseUrl: 'https://data.cambeerfestival.app/cbf2025',Or configure it dynamically based on the platform (see lib/services/beer_api_service.dart).