Skip to content

Add online rating service design plan - #210

Merged
richardthe3rd merged 8 commits into
mainfrom
claude/plan-rating-service-WyIZZ
Feb 8, 2026
Merged

Add online rating service design plan#210
richardthe3rd merged 8 commits into
mainfrom
claude/plan-rating-service-WyIZZ

Conversation

@richardthe3rd

Copy link
Copy Markdown
Owner

Cloudflare Workers + D1 backend with Firebase Anonymous Auth
for cross-device identity. Covers API design, Flutter integration,
offline sync strategy, and phased implementation plan.

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa

Cloudflare Workers + D1 backend with Firebase Anonymous Auth
for cross-device identity. Covers API design, Flutter integration,
offline sync strategy, and phased implementation plan.

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
- Three environments: dev (PRs), staging (main), production (tags)
- Each with separate D1 database and Worker deployment
- Phase 0: add tests to existing data proxy Worker (CORS, parsing)
- Vitest + Miniflare testing pattern for both Workers
- Flutter unit/widget/E2E test coverage for rating features
- CI pipeline with test gates before each deployment stage

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
- Custom domain: yes, ratings.cambeerfestival.app for all environments
- Min ratings: server-configurable threshold, 0 for testing, tunable for prod
- Archival: read-only after festival end date, block new writes
- Distribution bar chart: defer to v2

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
Type 2 decision - start simple with copy in each Worker,
refactor to shared module if maintenance burden emerges.

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
Identifies 14 risks across high/medium/low severity:
- JWT verification complexity (spike candidate)
- Aggregate table consistency (needs recompute safety net)
- Firebase Auth web persistence limitations
- Missing EnvironmentService.isStaging() method
- Festival end-date source undefined for ratings Worker
- Batch endpoint caching problem
- Rate limiting won't work in-memory
- Existing local ratings migration gap
- Bundle size impact of firebase_auth on web

Includes 5 pre-Phase-1 actions to reduce risk.

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
Existing local ratings are for past festivals. Online ratings will
launch with a new festival so everyone starts fresh. No migration needed.

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
Rate limiting: launch with DB constraint only (UNIQUE per user/drink),
upgrade to D1 rate-limit table if abuse observed.

Batch endpoint: aggregates only (no per-user data), fully cacheable
with 60s TTL. Client uses local ratings. Add /mine in Phase 4.

Two pre-Phase-1 items remain: JWT spike and festival end-date source.

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
- Festival metadata: embed festivals.json at build time (same as data proxy)
- Drink ID validation: trust client, don't validate for launch
- Update decision summary table with all resolved decisions
- Update write path to validate festivalId against embedded data
- Clean up anti-abuse description (DB constraint only, no rate limiting)
- Only remaining pre-Phase-1 item: JWT verification spike

https://claude.ai/code/session_01SusUN4s9TQ4AqeoyoGAGYa
Copilot AI review requested due to automatic review settings February 8, 2026 14:38
@richardthe3rd
richardthe3rd merged commit 62ad309 into main Feb 8, 2026
5 checks passed
@richardthe3rd
richardthe3rd deleted the claude/plan-rating-service-WyIZZ branch February 8, 2026 14:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a detailed design document for introducing an online, cross-device rating system to the Cambridge Beer Festival app, centered on a Cloudflare Workers + D1 backend and Firebase Anonymous Auth for identity.

Changes:

  • Introduces an end-to-end architecture and API design for a ratings service (single-drink + batch endpoints).
  • Proposes Flutter-side integration (services/models/provider updates) including offline-first sync behavior.
  • Outlines deployment environments, migrations, and a phased implementation/testing plan.

Comment on lines +483 to +487
} else if (EnvironmentService.isStaging()) {
return 'https://ratings-staging.cambeerfestival.app';
} else {
return 'https://ratings-dev.cambeerfestival.app';
}

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example ratingsApiBaseUrl routing uses EnvironmentService.isStaging(), but EnvironmentService currently only exposes isProduction() and getEnvironmentName() (no isStaging). Update this snippet to match the real API (or explicitly call out that isStaging() must be added before implementing).

Suggested change
} else if (EnvironmentService.isStaging()) {
return 'https://ratings-staging.cambeerfestival.app';
} else {
return 'https://ratings-dev.cambeerfestival.app';
}
}
final envName = EnvironmentService.getEnvironmentName();
if (envName == 'staging') {
return 'https://ratings-staging.cambeerfestival.app';
}
// Default to dev / local
return 'https://ratings-dev.cambeerfestival.app';

Copilot uses AI. Check for mistakes.
Comment on lines +278 to +280
/// Submit or update a rating, returns updated aggregates
Future<DrinkRatingResult> ratedrink(String festivalId, String drinkId, int rating);

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnlineRatingService method name ratedrink doesn’t follow Dart camelCase / naming conventions used elsewhere (e.g., setRating, getRating). Consider renaming to something like rateDrink / setDrinkRating for consistency and readability.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +315 to +317
/// Queue a rating for sync (called when offline or as fire-and-forget)
Future<void> queueRating(String festivalId, String drinkId, int rating);

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RatingSyncService.queueRating is defined here as queueRating(String festivalId, String drinkId, int rating), but later the plan uses null rating to represent deletes. Consider updating the design to support deletes explicitly (e.g., separate queueRemoveRating or a queued action type that can represent remove).

Suggested change
/// Queue a rating for sync (called when offline or as fire-and-forget)
Future<void> queueRating(String festivalId, String drinkId, int rating);
/// Queue a rating set/update for sync (called when offline or as fire-and-forget)
Future<void> queueRating(String festivalId, String drinkId, int rating);
/// Queue removal of a rating (user cleared their rating)
Future<void> queueRemoveRating(String festivalId, String drinkId);

Copilot uses AI. Check for mistakes.
notifyListeners(); // UI updates immediately

// 2. Sync to server in background
_ratingSyncService.queueRating(currentFestival.id, drink.id, rating);

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the BeerProvider.setRating example, _ratingSyncService.queueRating(...) is called with rating where the variable is int? (null means remove). This won’t match a queueRating(..., int rating) signature—either update the sync API to accept null/removals, or call a dedicated remove-queue method when rating == null.

Suggested change
_ratingSyncService.queueRating(currentFestival.id, drink.id, rating);
if (rating == null) {
_ratingSyncService.queueRemoveRating(currentFestival.id, drink.id);
} else {
_ratingSyncService.queueRating(currentFestival.id, drink.id, rating);
}

Copilot uses AI. Check for mistakes.
Future<String> getIdToken() async {
final user = _auth.currentUser;
if (user == null) throw StateError('Not authenticated');
return await user.getIdToken() ?? '';

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthService.getIdToken() shows return await user.getIdToken() ?? '';, but User.getIdToken() returns a non-null Future<String> (so ?? '' is misleading). Returning an empty string on failure would also cause opaque 401s downstream; prefer throwing if token retrieval fails (or forcing a refresh when needed).

Suggested change
return await user.getIdToken() ?? '';
return await user.getIdToken();

Copilot uses AI. Check for mistakes.

## 10. Resolved Questions

1. **Custom domain?** Yes — `ratings.cambeerfestival.app` (+ `ratings-staging`, `ratings-dev`). Simplifies CORS since `*.cambeerfestival.app` patterns are already handled.

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan states the custom domain "simplifies CORS since *.cambeerfestival.app patterns are already handled", but the existing Worker’s CORS logic uses an explicit allowlist (e.g., https://cambeerfestival.app, https://staging.cambeerfestival.app) rather than a *.cambeerfestival.app wildcard. Update this wording so it doesn’t imply wildcard support that isn’t present today.

Suggested change
1. **Custom domain?** Yes — `ratings.cambeerfestival.app` (+ `ratings-staging`, `ratings-dev`). Simplifies CORS since `*.cambeerfestival.app` patterns are already handled.
1. **Custom domain?** Yes — `ratings.cambeerfestival.app` (+ `ratings-staging`, `ratings-dev`). Simplifies CORS since it can reuse the existing explicit cambeerfestival.app origin allowlist used by the Worker.

Copilot uses AI. Check for mistakes.
| Anti-abuse | DB UNIQUE constraint (one rating per user per drink) | Sufficient for festival scale; upgrade to rate-limit table if needed |
| Batch endpoint | Aggregates only (no per-user data) | Fully cacheable; client uses local ratings for "my rating" |
| Festival metadata | Embed `festivals.json` at build time (same as data proxy) | Single source of truth for IDs, end dates, validation |
| Drink ID validation | Trust client (don't validate) | No incentive to fabricate; orphan rows are harmless |

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decision summary says "Trust client (don't validate)" for drink IDs. Even if you don’t validate against the festival drink list, it’s still worth specifying basic constraints (max length/charset) to prevent malicious clients from creating unlimited distinct drink_ids that bloat D1 and batch responses.

Suggested change
| Drink ID validation | Trust client (don't validate) | No incentive to fabricate; orphan rows are harmless |
| Drink ID validation | Basic format/length validation only | Constrain charset + max length to avoid D1/batch bloat; no lookup against festival drink list |

Copilot uses AI. Check for mistakes.
4. Ratings follow the user across devices

---

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section numbering jumps from "## 6" to "## 8" (missing section 7). Consider renumbering (or adding a placeholder heading) so future references don’t drift as the plan changes.

Suggested change
## 7. Future Work (placeholder)
_This section is reserved for future expansion of the rating service design plan._

Copilot uses AI. Check for mistakes.
Comment on lines +943 to +944
| # | Action | Effort | Status |
|---|--------|--------|--------|

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The final summary table appears to have a duplicated header block (the | # | Action | Effort | Status | rows repeat twice). Removing the duplicate will make the "What to do before starting Phase 1" section easier to read.

Suggested change
| # | Action | Effort | Status |
|---|--------|--------|--------|

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants