How I Built Fruit Exchange, a Neighborhood Fruit Tree Map
A map-based community tool where neighbors list backyard fruit trees and others can find them — styled like a 90s farming game, verified with email, and built across three sessions with Claude Code
Ingredients
- Leaflet + OpenStreetMap — interactive map with no API key required (free)
- Supabase — database for listings, requests, exchanges, and email tokens (free tier)
- Resend — sends one-time email verification links to tree owners (free tier)
- Claude Code — terminal AI that wrote every component, route, and CSS class ($20/mo)
- Press Start 2P — pixel font from Google Fonts for the Harvest Moon aesthetic (free)
A Note on Format
Earlier posts on this site followed the build session-by-session — “Session 1, evening, 3 hours” — because those projects were built in a weekend or two. The next few posts cover longer arcs: weeks of incremental work, features that grew over time, projects that are still evolving. So the format shifts from build diary to retrospective. Same recipe card, same ingredients, same lessons — just a wider lens.
The Idea: Fruit Trees Are Everywhere and Nobody Knows
In Los Angeles, there are fruit trees in every other front yard. Lemon trees dropping hundreds of lemons. Fig trees with more fruit than a family can eat. Avocado trees that produce year-round. Most of that fruit falls on the ground and rots because the owner doesn’t need it and the neighbors don’t know it’s available.
I wanted a simple tool: a map where you can see which houses near you have fruit trees, what’s growing, and how to connect with the owner. Not a full marketplace. Not an app with logins and profiles. Just a map, a listing form, and a way to say “hey, can I grab some lemons?”
The twist: I wanted it to feel like a game. Specifically, like Harvest Moon on the SNES — pixel fonts, parchment-colored dialog boxes, chunky buttons. If you’re going to build a fruit tree finder, it should feel like walking into a village market, not filling out a government form.
Making It Feel Like a Village Market
The design decision came before any code. I told Claude: “style this like Harvest Moon SNES dialog boxes — pixel font, parchment backgrounds, chunky borders.” That one prompt shaped the entire visual language.
🔧 Developer section: Harvest Moon CSS
.hm-dialog— parchment-colored dialog box with a pixel-style border.hm-title— headers in Press Start 2P pixel font.hm-btn,.hm-btn-red,.hm-btn-gold— chunky buttons with hover states.hm-input,.hm-select— styled form inputs that match the pixel aesthetic- All classes live in
globals.cssunder a dedicated Harvest Moon section
The font alone does most of the work. Press Start 2P is a free Google Font that looks exactly like 90s game UI text. Pair it with warm background colors and thick borders and you’re immediately in a different world than a typical web form.
Pixel fonts are hard to read below ~9px. That constraint forced every label and description to be short and direct. The UI ended up cleaner because the font wouldn’t let me be verbose.
Building the Map
Google Maps charges per load. Mapbox needs an API key. Leaflet with OpenStreetMap tiles is completely free, no key required, and looks great. The trade-off is that Leaflet is a client-side library — it only works inside the browser. Next.js, by default, tries to render pages on the server first before sending them to the browser (called server-side rendering, or SSR). Leaflet crashes during that server step because there’s no browser to render into.
🔧 Developer section: Leaflet + Next.js
FruitMap.tsxis loaded with a “dynamic import” that tells Next.js: skip this component during server rendering, only load it in the browser- Leaflet tries to access
window(a browser-only object) on import, which crashes during server rendering. The dynamic import is the standard fix. - React’s strict mode runs certain code twice during development (on purpose, to catch bugs). That double-run triggers Leaflet’s “Map container is already initialized” error because it tries to create the map twice. Fixed with a guard that checks if the map already exists before creating another one.
- Map centers on Los Angeles by default, with fruit tree markers loaded from Supabase on mount
Each fruit type gets its own marker color on the map. Clicking a marker opens a popup with the tree details, the owner’s Venmo link (if they added one), and an option to record an exchange. The map is the entire interface — there’s no separate list view or search page.
The Trust Problem: Who Can List a Tree?
Anyone can look at the map. But listing a tree means putting an address on a public page. That needs to be a real person with a real email who actually lives there — not someone listing a stranger’s house as a prank.
The solution: email verification for owners only. When you submit a listing, Resend sends a one-time verification link to your email. Click it, and the listing goes live. Don’t click it, and the listing never appears on the map. The token expires after 24 hours.
🔧 Developer section: Verification flow
- Owner submits listing → row created in
fruit_listingswithverified: false - Simultaneously, a token row is created in
email_verifications(UUID token, 24hr expiry) - Resend sends an email with a link to
/api/fruit-exchange/verify?token=... - Clicking the link hits the verify API route, which checks the token, marks the listing as verified, and redirects back to the map
- Unverified listings never appear in the map query — the
GET /api/fruit-exchange/listingsroute filters onverified: true
Requesters, on the other hand, are fully anonymous. If you see an address that doesn’t have a tree listed but you know one’s there, you can submit a request — no email, no account. The request shows up as a different marker style so the community can see where demand exists.
Four Tables, Four Purposes
The data model is intentionally flat. Four Supabase tables, each independent — no complex relationships between them. Each table handles one concern:
- fruit_listings — verified owner tree listings (address, fruit type, Venmo link, notes)
- fruit_requests — anonymous community requests (“I think there’s a fig tree at this address”)
- fruit_exchanges — successful pickup records with optional ratings (social proof)
- email_verifications — one-time tokens with 24-hour expiry for owner verification
Ten fruit types are supported: apple, lemon, lime, orange, grapefruit, fig, avocado, persimmon, stone fruit, and “other” with a free-text field. The list came from walking Goose around the neighborhood and writing down what I saw.
Six Components, One Page
The entire feature lives on a single page at /fruit-exchange. No routing, no sub-pages. Everything is a modal or popup layered on top of the map:
- FruitMap — the Leaflet map with tree markers and popups
- WelcomeModal — first-visit explainer (“Pick Responsibly →”)
- TreePopup — detail view when you click a tree marker
- ListTreeModal — 3-step form for owners to add a tree
- RequestModal — simple form for anonymous tree requests
Five API routes handle the backend: listings (GET all / POST new), requests, verification, exchange recording, and a stats endpoint that returns counts for the welcome screen.
Final Output
Fruit Exchange went live on March 1 and has been running since. The map loads, listings appear, verification emails send, and the Harvest Moon styling gets comments every time someone sees it for the first time.
What went fast
- The Harvest Moon CSS — one prompt to Claude describing the aesthetic, and the full set of
.hm-*classes came back ready to use. The pixel font and parchment colors made every component feel cohesive without a design system. - Supabase table setup — four tables, each under 10 columns. No complex relations. The schema took 15 minutes because I kept it flat on purpose.
- The verification flow — Resend was already set up from earlier projects. The token-and-verify pattern is simple enough that Claude generated it correctly on the first pass.
What needed patience
- Leaflet + React strict mode — the “Map container is already initialized” error only appears in development because React strict mode double-fires effects. The fix (a
_leaflet_idguard) isn’t obvious if you haven’t seen it before. This one bug ate 45 minutes. - Font sizing at small scales — Press Start 2P becomes unreadable below ~9px. Initial designs had 7px labels that looked great on a MacBook screen but were impossible to read on a phone. Bumped the minimum to 8.5px after the first round of feedback.
- Welcome modal copy — the original CTA said “Enter Village →” which was fun but confusing. Changed to “Pick Responsibly →” after feedback that it wasn’t clear what the button did. Small copy change, big clarity improvement.
The thing I like most about Fruit Exchange is that it’s not a tool for me. The server articles, the Garmin recaps, the alert system — those are all infrastructure I built for myself. This is the first feature on the site that exists entirely for other people. Whether anyone uses it at scale doesn’t matter yet. The fact that it works and it’s live means the idea is testable, and that’s the point.