Drop a message anywhere on the map. Someone will find it.
A mobile-first PWA where anyone can leave geo-tagged notes on a map — no account required to post. Others can like, save, and reply. Built with soft colors, spring animations, and as little friction as possible.
For anyone:
- Tap anywhere on the map to pin a message (up to 280 characters) at that exact location
- Messages appear instantly for everyone via realtime websocket — no refresh
- Like any message anonymously (tracked by device fingerprint, syncs to account on sign-in)
- Share any message — native share sheet on mobile, clipboard fallback on desktop
- Search any address or place — map flies to it, powered by OpenStreetMap Nominatim (no API key needed)
- Browse a heatmap overlay: Activity / Popular (likes-weighted, recency-decayed) / Fresh (< 24h)
- Messages are color-coded by age: rose → amber → sage → gray
- Pin clusters at low zoom; spiderfies as you zoom in
For signed-in users:
- Save messages across devices (bookmark survives expiry with "no longer on map" badge)
- Reply privately to any author — scoped to the original map message, no public threads
- Get email + push notifications on new replies and likes
- Choose a display name shown to people you message
All messages expire. You pick the TTL when posting: 1 hour, 6 hours, 24 hours, 7 days, or 30 days. Nothing here is permanent.
Proximity alerts. If someone drops a message within 500 meters of you while you have the app open, a brief pill notification slides in from the top.
Bilingual. Full English and Argentine Spanish (voseo) support, auto-detected from browser.
| Layer | Choice |
|---|---|
| Frontend | Vite + React 18 |
| Map | Leaflet.js (CartoDB Positron tiles) + leaflet.markercluster + leaflet.heat |
| Styling | Tailwind CSS + Framer Motion (spring physics) |
| Backend | Supabase (Postgres + Auth + Realtime + Edge Functions) |
| Resend (via Edge Functions) | |
| Hosting | Vercel |
git clone https://github.com/horacio/mapmessage
cd mapmessage
nvm use # uses .nvmrc (Node 22)
npm installCreate a free project at supabase.com, then:
- Open SQL Editor in your Supabase dashboard
- Paste and run
supabase/schema.sql
cp .env.example .envFill in .env:
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
VITE_SUPABASE_ANON_KEY=your_anon_public_key
VITE_VAPID_PUBLIC_KEY=your_vapid_public_key # for web push
# Only needed for seed/reset scripts — never exposed to browser
VITE_SUPABASE_SERVICE_KEY=your_service_role_keyFind the Supabase values at Settings → API. Generate VAPID keys with npx web-push generate-vapid-keys.
Site URL (for magic link redirects):
- Supabase Dashboard → Authentication → URL Configuration
- Set Site URL to your production domain (e.g.
https://mapmessage.vercel.app)
Email template (optional but recommended):
- Supabase → Authentication → Email Templates → Magic Link
- Subject:
Your link to MapMessage - Paste
supabase/email_magic_link.html
Database webhooks (for push + email notifications):
| Table | Event | Function URL |
|---|---|---|
private_messages |
INSERT | .../functions/v1/notify-private-message |
likes |
INSERT | .../functions/v1/notify-like |
Both need header: Authorization: Bearer <service_role_key>
npm run devOpen http://localhost:5173.
npm run dev # Start dev server
npm run build # Production build → dist/
npm run seed # Populate DB with 100 demo messages (Buenos Aires)
npm run reset-db # Wipe all user content (confirms before deleting)
npm run generate-icons # Regenerate icon-192.png + icon-512.png from icon.svgseed and reset-db require VITE_SUPABASE_SERVICE_KEY in .env.
| Thing | Limit |
|---|---|
| Public message length | 280 characters |
| Private message length | 1,000 characters |
| Display name | 40 characters |
| Post rate limit | 30 seconds (client + DB enforced) |
| Messages loaded on init | 500 most recent |
| Messages per conversation | 100 per load |
| Proximity alert radius | 500 meters |
| Message expiry options | 1h / 6h / 24h / 7d / 30d |
src/
├── components/
│ ├── AddToHomeScreen.jsx # PWA install prompt (iOS instructions + Android native)
│ ├── AuthModal.jsx # Magic link auth (context-aware: save / reply / messages)
│ ├── BottomNav.jsx # Tab navigation with unread + saved badges
│ ├── ComposeSheet.jsx # Write + post a message (with expiry picker)
│ ├── ConversationView.jsx # Private message thread (read receipts, realtime)
│ ├── DisplayNameModal.jsx # First sign-in name setup
│ ├── MapView.jsx # Leaflet map, pin markers, heatmap, clustering
│ ├── MessageSheet.jsx # Message detail + like / save / share / reply / flag
│ ├── MessagesView.jsx # Private conversations inbox
│ ├── ProximityAlert.jsx # Slide-in alert when someone posts nearby
│ ├── PushPrompt.jsx # Web push consent prompt
│ ├── SearchBar.jsx # Address geocoding bar (Nominatim, debounced)
│ ├── SavedView.jsx # Bookmarked messages (expired ones preserved)
│ ├── Sheet.jsx # Reusable drag-to-dismiss bottom sheet
│ └── WelcomeSplash.jsx # First-visit intro (one-time, localStorage-gated)
├── context/
│ └── AppContext.jsx # All state, Supabase queries, realtime, auth, push
├── i18n/
│ ├── index.js # i18next config (LanguageDetector, localStorage key)
│ ├── en.json # English strings
│ └── es-AR.json # Argentine Spanish (voseo throughout)
├── lib/
│ ├── fingerprint.js # Anonymous device UUID (localStorage)
│ ├── geo.js # Haversine distance + formatDistance()
│ ├── push.js # Web Push subscribe / unsubscribe (VAPID)
│ ├── supabase.js # Supabase client
│ └── timeago.js # timeAgo() / timeUntil() / isExpired() — i18n-aware
└── index.css # Tailwind + Leaflet + markercluster + animation CSS
supabase/
├── schema.sql # Full schema, RLS policies, triggers
├── functions/
│ ├── notify-private-message/ # Email (Resend) + push on new DM
│ ├── notify-like/ # Push notification on like received
│ └── send-push-notification/ # Shared push dispatch (VAPID, stale cleanup)
└── email_magic_link.html # Branded magic link email template
scripts/
├── seed.mjs # 100 demo messages across Buenos Aires
├── reset-db.mjs # Wipe all user content (y/N prompt)
└── generate-icons.mjs # Generate PNG icons from icon.svg (requires sharp)
public/
├── manifest.json # PWA manifest (standalone, portrait, icons)
├── sw.js # Service worker (push events only, no offline cache)
├── icon.svg # Source icon
├── icon-192.png # Generated — used by push notifications
└── icon-512.png # Generated — used by PWA install
npm run build # outputs to dist/Deploy dist/ to any static host. No server required — Supabase handles the entire backend. Edge Functions deploy separately:
supabase functions deploy notify-private-message
supabase functions deploy notify-like
supabase functions deploy send-push-notificationSet Edge Function secrets in the Supabase dashboard: RESEND_API_KEY, VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY, SUPABASE_SERVICE_ROLE_KEY.