Context
The proto-first personal API references Festival (festivals/{f}) and Drink (festivals/{f}/drinks/{d}) parent resources, but until now nothing actually defined or served them — they existed only as resource_definition stubs in drink_entry.proto. Meanwhile the catalogue is served as static JSON feeds (data.cambeerfestival.app/{festivalId}/{category}.json), fetched directly by the app via BeerApiService.
So we had two parallel worlds: a structured AIP personal API and an ad-hoc static feed, joined only by ID convention.
What's been done (branch claude/festival-api-drinks-d1au01, PR #433)
Drafted the contract for a read-only catalogue API to partner the personal API, without committing to a server. Everything lives in one package, cambeerfestival.festival.v1alpha, exposing two services over the same resource hierarchy (mirroring the app's two destinations):
CatalogService ("The Festival") — ListFestivals/GetFestival, ListDrinks (filter / order_by / paginated)/GetDrink, ListProducers/GetProducer. All reads.
MyFestivalService ("My Festival") — the existing personal DrinkEntry + aggregate DrinkSummary RPCs.
Resources: Festival (festivals/{f}), Producer (festivals/{f}/producers/{p}), Drink (festivals/{f}/drinks/{d}). Drink carries a ProducerReference (producer resource name + display_name denormalised for cheap rendering), keeping brewery detail normalised on the Producer resource — matching the producer-keyed feed.
Details:
- Catalogue resources are defined canonically here; the duplicate
resource_definition stubs were removed from drink_entry.proto (same-package references resolve directly).
- Every catalogue data field is
OUTPUT_ONLY; Festival.data_base_uri still points at the existing static feeds.
buf lint, AIP api-linter, and OpenAPI generation all pass; proto tasks remain dev-only (mise.dev.toml), not in CI.
Decision still open
The contract exists; the implementation is deliberately deferred. The static feeds remain the right tool for read-mostly, bounded, CDN-friendly catalogue data, so a real catalogue service is only worth building if/when we need:
- server-side search/filter across festivals, or
- a single denormalised "grid in one round-trip" call (catalogue +
DrinkSummary + caller's DrinkEntry), or
- a partner-facing surface beyond the raw feeds.
Until then, the documented REST binding can be served by the existing static feeds (or a thin reshaping Worker).
Resolved
- Namespace/naming: unified everything under one package
cambeerfestival.festival.v1alpha with two services, CatalogService + MyFestivalService. Promotion is granular down to the individual RPC via the version segment (the version is the package; a stable version carries only the ready methods, alpha keeps a copy of the stable surface plus what's still in flux), so one package does not couple stability. No X.XService stutter. See proto/buf.yaml / proto/README.md.
- Producer as a resource: the feed is producer-keyed, so
Producer is now a first-class resource and Drink references it (ProducerReference) instead of embedding the full record. Supports brewery-directory browsing and avoids duplicating brewery metadata across drinks.
Follow-ups to consider
Context
The proto-first personal API references
Festival(festivals/{f}) andDrink(festivals/{f}/drinks/{d}) parent resources, but until now nothing actually defined or served them — they existed only asresource_definitionstubs indrink_entry.proto. Meanwhile the catalogue is served as static JSON feeds (data.cambeerfestival.app/{festivalId}/{category}.json), fetched directly by the app viaBeerApiService.So we had two parallel worlds: a structured AIP personal API and an ad-hoc static feed, joined only by ID convention.
What's been done (branch
claude/festival-api-drinks-d1au01, PR #433)Drafted the contract for a read-only catalogue API to partner the personal API, without committing to a server. Everything lives in one package,
cambeerfestival.festival.v1alpha, exposing two services over the same resource hierarchy (mirroring the app's two destinations):CatalogService("The Festival") —ListFestivals/GetFestival,ListDrinks(filter / order_by / paginated)/GetDrink,ListProducers/GetProducer. All reads.MyFestivalService("My Festival") — the existing personalDrinkEntry+ aggregateDrinkSummaryRPCs.Resources:
Festival(festivals/{f}),Producer(festivals/{f}/producers/{p}),Drink(festivals/{f}/drinks/{d}).Drinkcarries aProducerReference(producer resource name +display_namedenormalised for cheap rendering), keeping brewery detail normalised on theProducerresource — matching the producer-keyed feed.Details:
resource_definitionstubs were removed fromdrink_entry.proto(same-package references resolve directly).OUTPUT_ONLY;Festival.data_base_uristill points at the existing static feeds.buf lint, AIPapi-linter, and OpenAPI generation all pass; proto tasks remain dev-only (mise.dev.toml), not in CI.Decision still open
The contract exists; the implementation is deliberately deferred. The static feeds remain the right tool for read-mostly, bounded, CDN-friendly catalogue data, so a real catalogue service is only worth building if/when we need:
DrinkSummary+ caller'sDrinkEntry), orUntil then, the documented REST binding can be served by the existing static feeds (or a thin reshaping Worker).
Resolved
cambeerfestival.festival.v1alphawith two services,CatalogService+MyFestivalService. Promotion is granular down to the individual RPC via the version segment (the version is the package; a stable version carries only the ready methods, alpha keeps a copy of the stable surface plus what's still in flux), so one package does not couple stability. NoX.XServicestutter. Seeproto/buf.yaml/proto/README.md.Produceris now a first-class resource andDrinkreferences it (ProducerReference) instead of embedding the full record. Supports brewery-directory browsing and avoids duplicating brewery metadata across drinks.Follow-ups to consider
availableStyles/styleCountsMapetc. client-side) — only needed if the server should own those.mise.dev.tomlinto CI.