Find your cafΓ©'s twin, anywhere.
Elsebrew is a fake-door MVP that helps coffee lovers discover cafΓ©s in new cities that match the vibe of their favorite local spots. Built with Next.js 14, Google Maps Platform, and AI-powered matching.
π New here? See docs/FILE_TREE.txt for a visual overview of the project structure.
π Looking for specific docs? See docs/DOCS_INDEX.md for documentation navigation.
π Latest Features:
- β Place interaction tracking for all users (anonymous + logged-in) - see docs/PLACE_TRACKING_FEATURE.md
- β Atmosphere & amenity fields (outdoor seating, takeout, etc.) - see docs/ATMOSPHERE_FIELDS_IMPLEMENTATION.md
- Node.js 18+
- Google Cloud Platform account
- OpenAI API key (or compatible LLM provider)
- (Optional) Google Analytics 4 property
- (Optional) Mailchimp account
- (Optional) Buy Me A Coffee account
- Clone and install dependencies:
npm install- Set up environment variables:
Copy .env.example to .env.local:
cp .env.example .env.localThen edit .env.local with your actual keys (see Configuration section below).
- Run the development server:
npm run devOpen http://localhost:3000 in your browser.
Elsebrew requires two API keys for security best practices:
- Client-side key (
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY) - for Maps JavaScript API and Places autocomplete in the browser - Server-side key (
GOOGLE_MAPS_API_KEY) - for Places API (New) calls from Next.js API routes
Quick Setup:
See docs/GOOGLE_API_KEYS_SETUP.md for detailed step-by-step instructions.
TL;DR:
- Go to Google Cloud Console
- Enable these 5 APIs: Maps JavaScript API, Places API, Geocoding API, Maps Embed API, and Places API (New)
- Create two API keys with different restrictions:
- Client key: HTTP referrers + ALL 5 APIs (including Places API New for searchByText)
- Server key: IP addresses (or None) + Places API (New) only
- Add to
.env.local:
# Client-side (for Maps JS API)
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=AIza...
# Server-side (for Places API)
GOOGLE_MAPS_API_KEY=AIza...- In Google Cloud Console, go to APIs & Services β Credentials
- Create credentials β OAuth 2.0 Client ID
- Application type: Web application
- Authorized JavaScript origins:
http://localhost:3000(development)https://yourdomain.com(production)
- Add to
.env.local:
NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID=123456789-abc...apps.googleusercontent.com
- Sign up at OpenAI
- Create an API key
- Add to
.env.local:
OPENAI_API_KEY=sk-...
Note: The app uses gpt-4o-mini for cost efficiency. Each cafΓ© match explanation costs ~$0.0001.
- Create a GA4 property at Google Analytics
- Get your Measurement ID (format:
G-XXXXXXXXXX) - Add to
.env.local:
NEXT_PUBLIC_GA4_MEASUREMENT_ID=G-XXXXXXXXXX
If left empty, analytics tracking will be disabled.
- Create a Mailchimp audience
- Create an embedded form
- Copy the form action URL (looks like
https://XXXXX.usX.list-manage.com/subscribe/post?u=...&id=...) - Add to
.env.local:
MAILCHIMP_FORM_ACTION=https://XXXXX.usX.list-manage.com/subscribe/post?u=...&id=...
Note: The current implementation shows a success message without actually POSTing to Mailchimp. To enable real submissions, update components/home/EmailSignup.tsx.
- Create an account at Buy Me A Coffee
- Copy your profile URL
- Add to
.env.local:
NEXT_PUBLIC_BUYMEACOFFEE_URL=https://www.buymeacoffee.com/yourname
elsebrew/
βββ app/
β βββ about/ # About page
β βββ api/
β β βββ reason/ # LLM reasoning API route
β βββ privacy/ # Privacy policy
β βββ results/ # Search results page
β βββ saved/ # Saved cafΓ©s page
β βββ terms/ # Terms of service
β βββ layout.tsx # Root layout
β βββ page.tsx # Home page
β βββ globals.css # Global styles
βββ components/
β βββ auth/ # Google Sign-In
β βββ home/ # Home page components
β βββ results/ # Results page components
β βββ shared/ # Shared components (Header, Footer, etc.)
βββ lib/
β βββ analytics.ts # Analytics tracking
β βββ maps-loader.ts # Google Maps loader
β βββ places-search.ts # CafΓ© search logic
β βββ scoring.ts # Scoring & keyword matching
β βββ storage.ts # localStorage utilities
βββ types/
β βββ index.ts # TypeScript types
βββ public/
βββ images/ # Static images
-
User Input:
- Source cafΓ© (Google Places Autocomplete for establishments)
- Destination city (Google Places Autocomplete for cities)
- Optional vibe toggles (roastery, light roast, etc.)
-
Candidate Generation:
- Geocode destination city to get center + bounds
- Build search query from vibe keywords
- Use Google Places Text Search within destination bounds
- Fetch top 30 candidates, then detailed info for top 12
-
Scoring:
- Base score: rating + log(review count)
- Bonus for price level match (Β±1)
- Keyword matching (roastery, specialty coffee, etc.)
- Opening hours overlap (if Night-owl selected)
- Photo presence bonus
- Sort by score, take top 8
-
LLM Reasoning:
- For each result, call
/api/reasonwith source/candidate data - OpenAI generates 1-2 sentence explanation
- Results cached for 5 minutes
- For each result, call
-
Display:
- List view (left) with photo, rating, tags, reasoning
- Map view (right) with numbered markers
- Click to open details drawer with embedded Google Maps Place card
- Client-side: Google Maps API calls, autocomplete, map rendering
- Server-side: LLM reasoning API route (Edge runtime)
- Storage: Saved cafΓ©s + user profile in localStorage (no backend database)
- Analytics: GA4 or Plausible for event tracking
This app respects Google Maps Platform Terms of Service:
- No persistent storage: Place names, reviews, photos are NOT stored server-side beyond ephemeral caching (5 min for LLM reasoning)
- Storing place_id is OK: We only persist
place_idin localStorage for saved cafΓ©s - Required attributions: "Powered by Google" shown in footer; embedded maps include Google branding
- API key restrictions: Lock your key to your domain and enable only required APIs
- Respect quotas: Use
fieldsparameter to minimize costs; debounce autocomplete
Cost estimates (as of 2024):
- Text Search: $32/1000 requests
- Place Details: $17/1000 requests (with minimal fields)
- Map loads: $7/1000 loads
- Autocomplete: $2.83/1000 sessions
For MVP validation with 100 searches/day: **$10-20/month**
The app tracks these events for validation:
view_home- Home page viewclick_sign_in- Google Sign-In clickedsearch_submit- Search submitted (includes source, dest, toggles)results_loaded- Results displayed (includes count, latency)result_click- Result clicked (includes rank, place_id)result_save_google- Embedded Save button clickedresult_open_gmaps- "Open in Google Maps" clickedbuy_me_coffee_click- Buy Me A Coffee button clickedemail_subscribe_submit- Email signup submittedcta_upgrade_click- "Pro coming soon" clicked
View these in GA4 Realtime or your Plausible dashboard.
- Espresso:
#5B4636(primary brand color) - Espresso Dark:
#3D2E24(hover states) - Off-white:
#FAFAF8(background) - Charcoal:
#2D2D2D(text)
- Headings: Georgia serif
- Body: System sans-serif
- Code/mono: ui-monospace
- Buttons:
.btn-primary,.btn-secondary(rounded-2xl, subtle shadows) - Cards:
.card(white bg, rounded-2xl, hover shadow) - Inputs:
.input-field(rounded-xl, focus ring)
- Framer Motion used for:
- Page transitions
- Card hover states
- Drawer slide-in
- Staggered list animations
- Push to GitHub
- Import project in Vercel
- Add environment variables in Vercel dashboard
- Deploy
Important: Update your Google Maps API key restrictions to include your Vercel domain.
This is a standard Next.js 14 app and can be deployed to:
- Netlify
- AWS Amplify
- Cloudflare Pages
- Any Node.js hosting
npm run devnpm run build
npm run startnpm run lintTo use a different LLM (e.g., Claude, Gemini):
- Update
app/api/reason/route.ts - Swap OpenAI client for your provider's SDK
- Adjust prompt template as needed
- Update
.env.localwith new API key
Edit components/home/SearchPanel.tsx - vibeOptions array:
const vibeOptions = [
{ key: 'roastery', label: 'Roastery', icon: 'π₯' },
// Add your own toggles here
];Then update lib/scoring.ts - buildSearchKeywords() to add corresponding keywords.
Edit lib/scoring.ts - scoreCafe() function to tweak weights:
// Example: increase roastery bonus from +2 to +5
if (vibes.roastery && combinedText.includes('roast')) {
score += 5; // was 2
matchedKeywords.push('Roastery');
}This is a validation project. The core functionality is real (live Google Maps results, working map, AI reasoning), but it's designed to:
- Test product-market fit - Do people actually want this?
- Capture demand signals - Email signups, Buy Me A Coffee clicks
- Validate pricing assumptions - "Pro coming soon" clicks
- Stay lean - No backend database, minimal infra costs
What's real:
- Google Maps search and results
- Interactive map with markers
- LLM-generated explanations
- Save to Google Maps (via embedded Place card)
- Analytics tracking
What's fake-door:
- Pro tier (button does nothing, just tracks clicks)
- Email signup (shows success but doesn't actually POST to Mailchimp by default)
- No user accounts (localStorage only)
- No backend database
"Something went wrong - Failed to get source place details"
This is a common issue when deploying to AWS Amplify. See the comprehensive guide:
π docs/DEPLOYMENT_TROUBLESHOOTING.md
Quick fixes:
- Set
NEXT_PUBLIC_GOOGLE_MAPS_API_KEYin AWS Amplify environment variables - Add your Amplify domain to Google Maps API key restrictions:
- Use
https://*.YOUR_APP_ID.amplifyapp.com/*(e.g.,https://*.d1a2b3c4d5e6.amplifyapp.com/*) β οΈ Don't usehttps://*.amplifyapp.com/*- that's too permissive!
- Use
- Redeploy after changing environment variables
- Check
NEXT_PUBLIC_GOOGLE_MAPS_API_KEYin.env.local - Verify APIs are enabled in Google Cloud Console
- Check browser console for errors
- Ensure API key restrictions include
http://localhost:3000/*
- Fixed: Token expiration handling updated to allow 24-hour grace period
- If you still see 401 errors, try signing out and signing in again
- Check browser console for
[Auth]log messages
- Verify Places API is enabled
- Check API key restrictions
- Open browser console and look for 403/API key errors
- Check
OPENAI_API_KEYin.env.local - Verify you have credits in your OpenAI account
- Check
/api/reasonroute logs in terminal
- Check
NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_IDin.env.local - Verify authorized origins in OAuth consent screen
- Check browser console for errors
See docs/DEPLOYMENT_TROUBLESHOOTING.md for detailed debugging steps, error message reference, and AWS-specific solutions.
MIT License - feel free to use this as a template for your own projects.
This is a validation project! We'd love your feedback:
- β Star this repo if you find it useful
- π Open an issue for bugs
- π‘ Submit feature requests
- β Buy us a coffee to support development
Made with β€οΈ by Elsebrew β’ Powered by Google