Lost and found in Iceland
fundid.is
Map-based PWA, anonymous contact relay, no accounts. Free and open.
Anyone can post a lost or found item, pin it on the map, and get contacted through an anonymous email relay. Neither party sees the other's email. Claim codes verify item ownership when closing a listing. Bilingual (Icelandic/English).
12 categories: phone, wallet, keys, bag, glasses, clothing, jewelry, documents, electronics, pet, bicycle, other.
Browser (SvelteKit PWA)
├── Supabase (PostgreSQL + PostGIS, RLS, pg_net)
│ └── Edge Function: send-email (Resend API)
├── Cloudflare R2 (images, zero egress)
├── Cloudflare Worker: data-retention (daily cron)
└── MapLibre GL + OSM tiles
- SvelteKit 5 on Cloudflare Pages
- Supabase handles auth-free DB access with Row Level Security
- PostGIS spatial index for nearby item queries
- R2 stores images with CDN caching. KV backs upload rate limiting
- All email goes through Supabase Edge Functions via pg_net (async HTTP from Postgres)
- Data retention worker runs daily at 3am UTC
| Layer | What |
|---|---|
| Framework | SvelteKit 5, Svelte 5 |
| Styling | Tailwind CSS 4 |
| Database | Supabase (PostgreSQL 15, PostGIS) |
| Storage | Cloudflare R2 |
| Hosting | Cloudflare Pages + Workers |
| Maps | MapLibre GL + OpenStreetMap |
| Resend (via Supabase Edge Function) | |
| i18n | Tolgee |
| Analytics | PostHog (EU, cookieless) |
| Icons | Lucide |
- pnpm (npm install fails on this repo)
- Supabase CLI
- Cloudflare account with R2 enabled
git clone git@github.com:sandsower/fundid.git
cd fundid
pnpm installCopy the example env file and fill in your values:
cp .env.example .envRequired variables:
| Variable | Where | Purpose |
|---|---|---|
PUBLIC_SUPABASE_URL |
App | Supabase project URL |
PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY |
App | Supabase anon/public key |
PUBLIC_IMAGE_BASE_URL |
App | R2 image domain (e.g. https://img.fundid.is) |
PUBLIC_POSTHOG_KEY |
App (prod only) | PostHog project key |
RESEND_API_KEY |
Edge function | Resend email API key |
TOLGEE_API_KEY |
CLI only | For pulling/pushing translations |
supabase start
supabase db resetThis runs schema.sql and migrations 002 through 007. The migrations create:
itemstable with PostGIS geometry column and GiST indexcontact_messageswith threaded conversations and rate limitingresolve_attemptswith brute-force protection- RPC functions for contact relay, claim verification, data cleanup
- RLS policies on all tables
- R2 replaces Supabase Storage (migration 007 is intentionally empty)
Deploy the edge function:
supabase functions deploy send-emailpnpm dev # standard Vite dev server (no R2/KV bindings)
pnpm dev:cf # Cloudflare Pages dev with R2 + KV emulationpnpm dev works for most development. Use pnpm dev:cf when testing image uploads since that needs the R2 binding.
The wrangler.toml at the repo root handles all bindings:
- Production: R2 bucket
fundid-item-images, image domainimg.fundid.is - Preview: R2 bucket
fundid-item-images-dev, image domaindev-img.fundid.is - KV namespace
RATE_LIMITshared across environments
Deploy:
pnpm deploy:dev # builds + deploys to dev branch
pnpm deploy:prod # builds + deploys to main branchCreate the R2 buckets:
wrangler r2 bucket create fundid-item-images
wrangler r2 bucket create fundid-item-images-devCreate the KV namespace:
wrangler kv namespace create RATE_LIMIT
# Update the id in wrangler.toml with the returned valueCustom domains for R2 (configured via Cloudflare API or dashboard):
img.fundid.is->fundid-item-imagesdev-img.fundid.is->fundid-item-images-dev
cd workers/data-retention
wrangler deploy
wrangler secret put SUPABASE_URL
wrangler secret put SUPABASE_SERVICE_KEYRuns daily at 3am UTC. Deletes resolved items after 90 days, unresolved after 6 months. Cleans up orphaned R2 images.
fundid/
├── src/
│ ├── routes/
│ │ ├── +page.svelte # Home (map + list view)
│ │ ├── about/ # About, FAQ, privacy
│ │ ├── item/[id]/ # Detail, resolve, flyer
│ │ ├── reply/[id]/ # Reply to contact message
│ │ └── api/
│ │ ├── health/ # Health check endpoint
│ │ └── upload/ # R2 image upload (rate limited)
│ ├── lib/
│ │ ├── components/ # Svelte components
│ │ ├── stores/ # Svelte stores (items, filters)
│ │ ├── types/ # TypeScript interfaces
│ │ ├── utils/ # Geocoding, image compression, categories
│ │ ├── i18n/ # Translation files (is, en)
│ │ ├── supabase.ts # Client init
│ │ └── posthog.ts # Analytics init
│ ├── hooks.server.ts # CSP, security headers
│ └── app.d.ts # Platform types (R2, KV bindings)
├── supabase/
│ ├── schema.sql # Base schema + PostGIS
│ ├── 002-007_*.sql # Migrations
│ └── functions/send-email/ # Edge function (Resend)
├── workers/data-retention/ # Cloudflare Worker (cron)
├── scripts/
│ ├── load-test.sh # Load testing (uses hey)
│ └── posthog-dashboard.sh # PostHog dashboard IaC
├── wrangler.toml # Cloudflare Pages bindings
└── .tolgeerc # i18n config
All enforced server-side in Postgres RPC functions and the upload endpoint:
| Action | Limit | Window |
|---|---|---|
| Contact messages per item | 10 | 24 hours |
| Contact messages per sender | 20 | 24 hours |
| Resolve attempts per item | 5 | 1 hour |
| Image uploads per IP | 10 | 1 hour |
Translations are managed through Tolgee. Files live in src/lib/i18n/{languageTag}.json/. Default language is Icelandic.
Pull translations:
pnpm tolgee pullPush new keys:
pnpm tolgee pushPostHog (EU instance, eu.i.posthog.com). Cookieless with memory-only persistence. No autocapture, no session recording. Only fires when PUBLIC_POSTHOG_KEY is set (production).
Tracked events: page views, report form opens/submits, contact messages, item resolutions, filter changes, view toggles, language switches.
- Email addresses are never displayed publicly. All contact goes through the relay.
- No user accounts. No cookies. No tracking across sites.
- Items auto-delete: 90 days after resolution, 6 months if unresolved.
- Images stored on Cloudflare R2 (EU region), cleaned up with item deletion.
- Full data deletion available on request (privacy@fundid.is).
| Command | What it does |
|---|---|
pnpm dev |
Vite dev server |
pnpm dev:cf |
Dev with Cloudflare R2 + KV bindings |
pnpm build |
Production build |
pnpm deploy:dev |
Build + deploy to dev |
pnpm deploy:prod |
Build + deploy to production |
pnpm check |
TypeScript type checking |
pnpm check:watch |
Type checking in watch mode |
MIT