Skip to content

horacio/mapmessage

Repository files navigation

MapMessage

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.


What it does

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.


Stack

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)
Email Resend (via Edge Functions)
Hosting Vercel

Getting started

1. Clone and install

git clone https://github.com/horacio/mapmessage
cd mapmessage
nvm use   # uses .nvmrc (Node 22)
npm install

2. Supabase project

Create a free project at supabase.com, then:

  1. Open SQL Editor in your Supabase dashboard
  2. Paste and run supabase/schema.sql

3. Environment variables

cp .env.example .env

Fill 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_key

Find the Supabase values at Settings → API. Generate VAPID keys with npx web-push generate-vapid-keys.

4. Configure Supabase

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):

  1. Supabase → Authentication → Email Templates → Magic Link
  2. Subject: Your link to MapMessage
  3. 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>

5. Run

npm run dev

Open http://localhost:5173.


Scripts

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.svg

seed and reset-db require VITE_SUPABASE_SERVICE_KEY in .env.


Limits

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

Project structure

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

Deployment

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-notification

Set Edge Function secrets in the Supabase dashboard: RESEND_API_KEY, VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY, SUPABASE_SERVICE_ROLE_KEY.

About

Drop a message anywhere on the map. Someone will find it.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors