A location-based React application that helps users find the perfect dining spot based on their current location, time preference, and distance range.
This application acts as a randomization engine for restaurants. It aggregates restaurant data from various external sources to provide a unified profile. Users can search for dining options based on specific criteria (Dinner, Lunch, "Right Now") and interact with the results.
- Framework: React Router 7 (framework mode)
- Runtime: Cloudflare Workers
- Database: Cloudflare D1 (SQLite)
- ORM: Drizzle
- Styling: Tailwind CSS 4
- Language: TypeScript 5.9 (strict mode)
- Node.js >= 22.0.0
- npm
- Cloudflare account (for deployment)
npm installCreate a .dev.vars file with the required secrets:
BROULETTE_SESSION_SECRET=<generate with: openssl rand -hex 32>
BROULETTE_GOOGLE_PLACE_API_KEY=<your-google-api-key>
BROULETTE_TRIPADVISOR_API_KEY=<your-tripadvisor-api-key>npm run db:migrate:localnpm run devThe application will be available at http://localhost:5173.
npm testnpm run test:watchnpm run test:coverageCoverage reports are generated in the coverage/ directory.
npm test -- --run src/features/session.server/Tests are co-located with their source files using the .test.ts suffix:
src/features/
session.server/
csrf.ts
csrf.test.ts # Unit tests for CSRF
session.ts
session.test.ts # Unit tests for session
src/routes/
_.api.address-searches.ts
_.api.address-searches.test.ts # Route handler tests
npm run deploynpm run db:migrate:prodnpx wrangler secret put BROULETTE_SESSION_SECRET
npx wrangler secret put BROULETTE_GOOGLE_PLACE_API_KEY
npx wrangler secret put BROULETTE_TRIPADVISOR_API_KEYnpm run build
npm run previewThis project uses Drizzle for database management.
If you modify schema.ts, generate migrations:
npm run db:gen# Local
npm run db:migrate:local
# Production
npm run db:migrate:prod# Local database UI
npm run db:studio:local
# Production database UI
npm run db:studio:prodExport from D1:
npx wrangler d1 export broulette-eu --remote --no-data --output=./schema.sql
npx wrangler d1 export broulette-eu --remote --no-schema --output=./data.sqlImport to D1:
npx wrangler d1 execute broulette-eu --remote --file=./schema.sql
npx wrangler d1 execute broulette-eu --remote --file=./data.sqlNote: Data exports may have foreign key ordering issues. Consider disabling foreign key checks during import.
| Variable | Description | Required |
|---|---|---|
BROULETTE_SESSION_SECRET |
Secret for signing session cookies (generate with openssl rand -hex 32) |
Yes |
| Variable | Default | Description |
|---|---|---|
BROULETTE_NOMINATIM_ENABLED |
false |
Enable Nominatim for address search |
BROULETTE_PHOTON_ENABLED |
false |
Enable Photon for address search |
BROULETTE_OVERPASS_ENABLED |
false |
Enable Overpass for restaurant discovery |
BROULETTE_GOOGLE_PLACE_ENABLED |
false |
Enable Google Places for restaurant details |
BROULETTE_TRIPADVISOR_ENABLED |
false |
Enable TripAdvisor for restaurant details |
| Variable | Description |
|---|---|
BROULETTE_GOOGLE_PLACE_API_KEY |
Google Places API key |
BROULETTE_TRIPADVISOR_API_KEY |
TripAdvisor API key |
| Variable | Default | Description |
|---|---|---|
BROULETTE_NOMINATIM_INSTANCE_URLS |
https://nominatim.openstreetmap.org/search |
Comma-separated list of instance URLs |
BROULETTE_NOMINATIM_USER_AGENT |
BiteRoulette/<version> |
User agent for API requests |
BROULETTE_NOMINATIM_NUMBER_OF_ADDRESSES |
5 |
Maximum addresses to return |
BROULETTE_NOMINATIM_API_TIMEOUT |
5000 |
Request timeout in milliseconds |
BROULETTE_NOMINATIM_API_RETRIES |
3 |
Number of retries on failure |
| Variable | Default | Description |
|---|---|---|
BROULETTE_PHOTON_INSTANCE_URLS |
https://photon.komoot.io/api/ |
Comma-separated list of instance URLs |
BROULETTE_PHOTON_NUMBER_OF_ADDRESSES |
5 |
Maximum addresses to return |
BROULETTE_PHOTON_API_TIMEOUT |
5000 |
Request timeout in milliseconds |
BROULETTE_PHOTON_API_RETRIES |
3 |
Number of retries on failure |
| Variable | Default | Description |
|---|---|---|
BROULETTE_GOOGLE_PLACE_BASE_URL |
https://places.googleapis.com |
API base URL |
BROULETTE_GOOGLE_PLACE_API_SEARCH_RADIUS_IN_METERS |
100 |
Search radius for matching |
BROULETTE_GOOGLE_PLACE_API_PHOTO_MAX_WIDTH_IN_PX |
800 |
Maximum photo width |
BROULETTE_GOOGLE_PLACE_API_PHOTO_MAX_HEIGHT_IN_PX |
600 |
Maximum photo height |
BROULETTE_GOOGLE_PLACE_API_MAX_NUMBER_OF_ATTEMPTS_PER_MONTH |
10000 |
Rate limiting threshold |
| Variable | Default | Description |
|---|---|---|
BROULETTE_TRIPADVISOR_INSTANCE_URL |
https://api.content.tripadvisor.com/api/v1 |
API base URL |
BROULETTE_TRIPADVISOR_API_SEARCH_RADIUS_IN_METERS |
100 |
Search radius for matching |
BROULETTE_TRIPADVISOR_API_PHOTO_SIZE |
medium |
Photo size (small, medium, large) |
BROULETTE_TRIPADVISOR_API_MAX_NUMBER_OF_ATTEMPTS_PER_MONTH |
5000 |
Rate limiting threshold |
| Variable | Default | Description |
|---|---|---|
BROULETTE_OVERPASS_API_INSTANCE_URLS |
Multiple public instances | Comma-separated list of instance URLs |
BROULETTE_OVERPASS_API_TIMEOUT |
5000 |
Request timeout in milliseconds |
BROULETTE_OVERPASS_API_RETRIES |
3 |
Number of retries on failure |
| Variable | Default | Description |
|---|---|---|
BROULETTE_SEARCH_ENGINE_DISCOVERY_RANGE_INCREASE_METERS |
500 |
Range increase per iteration |
BROULETTE_SEARCH_ENGINE_MAX_DISCOVERY_ITERATIONS |
10 |
Maximum discovery iterations |
BROULETTE_SEARCH_ENGINE_CLOSE_RANGE_IN_METERS |
1500 |
"Close" distance range |
BROULETTE_SEARCH_ENGINE_CLOSE_TIMEOUT_IN_MS |
30000 |
Timeout for close searches |
BROULETTE_SEARCH_ENGINE_MID_RANGE_IN_METERS |
5000 |
"Mid-range" distance |
BROULETTE_SEARCH_ENGINE_MID_TIMEOUT_IN_MS |
45000 |
Timeout for mid-range searches |
BROULETTE_SEARCH_ENGINE_FAR_RANGE_IN_METERS |
15000 |
"Far" distance range |
BROULETTE_SEARCH_ENGINE_FAR_TIMEOUT_IN_MS |
60000 |
Timeout for far searches |
| Variable | Default | Description |
|---|---|---|
BROULETTE_TAGS_TO_EXCLUDE |
(empty) | Comma-separated tags to hide |
BROULETTE_TAGS_TO_PRIORITIZE |
(empty) | Comma-separated tags to show first |
BROULETTE_TAGS_MAXIMUM |
5 |
Maximum tags to display |
- Nominatim - OpenStreetMap's geocoding service
- Photon - Komoot's geocoding service (faster, location-biased)
- Overpass - OpenStreetMap query API for finding nearby restaurants
- Google Places - Rich details, photos, ratings
- TripAdvisor - Reviews, photos, cuisine details
The application implements several resilience patterns:
- Circuit Breaker: Prevents cascading failures when services are unavailable
- Load Balancer: Round-robin distribution across multiple service instances
- Retry with Exponential Backoff: Automatic retries with increasing delays
- Timeout: Configurable request timeouts per service
Files are organized with clear boundaries:
.server.ts- Server-only code (API calls, database, secrets).client.ts- Client-only code (DOM, geolocation, haptics)
Ensure the site is served over HTTPS. Geolocation API requires a secure context.
- Verify
BROULETTE_NOMINATIM_ENABLEDorBROULETTE_PHOTON_ENABLEDistrue - Check browser console for network errors
- Verify the service instances are accessible
This indicates a service failure. Check:
- API service availability
- Network connectivity
- Rate limiting (check API quotas)
- Verify
BROULETTE_OVERPASS_ENABLEDistrue - Try a different location (some areas have sparse OSM data)
- Increase the search range in preferences
- Verify
BROULETTE_GOOGLE_PLACE_ENABLEDorBROULETTE_TRIPADVISOR_ENABLEDistrue - Check API key validity
- Check rate limiting quotas
For production:
# Check current migration status
npx wrangler d1 execute broulette-eu --remote --command="SELECT * FROM __drizzle_migrations"
# Manually apply migrations if needed
npx wrangler d1 execute broulette-eu --remote --file=./drizzle/migrations/XXXX_migration.sql# Check types
npx tsc --noEmit
# Clear build cache
rm -rf node_modules/.vite
npm run buildEnable detailed logging by checking the browser console or Cloudflare Workers logs:
# Stream production logs
npx wrangler tail- Haptics do not work on Android?
- Move all the code about "cache" to a dedicated module?