A phone-first butterfly logging app for UK field recorders. Stand where you saw the butterfly, and Flutterby captures the species, a count, and an Ordnance Survey grid reference derived automatically from your GPS position — in a couple of taps. Built to fix the "Painted Lady in my garden, but where was the garden?" problem: every record carries a real location.
Two sections:
Log — record a visit as a report: day and time (default now) and a free-text location, then tap each butterfly where you see it. Every sighting is stamped with the GPS position at the moment it's logged, so the same species seen at three spots on a walk is three records with three grid references (adding another on the same 10 m square just tops up the count). Without a GPS fix — or with a grid reference typed by hand — sightings take the report's location instead. Save commits the whole visit in one go (with one-tap undo).
Reports — the recorder's own historic reports, each openable to edit the details, adjust counts, add/remove species, or delete the visit.
Identify — describe what you saw in plain words ("small orange butterfly
with dark spots on chalk downland in July") and get ranked suggestions with the
matched evidence shown. All reasoning is precomputed at build time (see
src/lib/idguide/): an LLM produced a per-species feature database and a
synonym lexicon against fixed controlled vocabularies; at runtime the app just
gazetteer-parses the text and scores every species with a weighted linear sum —
no model, no network, works offline.
- Automatic OS grid reference from the device's location (WGS84 → OSGB36 → National Grid), with precision that adapts to GPS accuracy.
- Every British butterfly (64 species) searchable by common or scientific name — the search sits directly above the grid.
- A starter grid of common, garden-friendly species on first use, replaced over time by the recorder's own most-logged species.
- Species photos from Wikimedia Commons (fetched at seed time).
| Layer | Choice |
|---|---|
| Frontend | React 19 + TypeScript, Vite, CSS Modules |
| Backend | Fastify 5 (serverless function on Vercel) |
| Database | Neon Postgres via Drizzle ORM |
| Deployment | Vercel |
The React app talks to the same relative /api/* paths in development (via a
Vite proxy) and in production (via a Vercel rewrite to the Fastify function), so
nothing changes between the two.
npm installCreate a Postgres database at neon.tech (or via the Vercel Marketplace) and copy the connection string.
cp .env.example .env
# then edit .env and paste your DATABASE_URLnpm run db:push # creates the butterflies + sightings tables
npm run db:seed # inserts the 60 species and fetches their imagesnpm run dev # Vite on :5173, Fastify API on :3001Open http://localhost:5173. On a phone you'll be prompted for location access; on a desktop you can still log sightings (the grid reference will just be blank/approximate).
- Push this repo to GitHub and import it into Vercel (framework preset: Vite
— the
api/function and rewrites come fromvercel.json). - Add
DATABASE_URLas a Vercel environment variable (or install a Neon integration from the Marketplace, which sets it for you). - Deploy. Run
npm run db:push/npm run db:seedonce against the same database (locally with the productionDATABASE_URL, orvercel env pull).
Login is never required. By default a recorder is anonymous — an id kept in the browser groups their own sightings on that device. Signing in exists only so a recorder can sync across devices (e.g. log on a phone, review on a laptop).
- Auth is Better Auth, self-hosted in the same
Neon database (no third-party service, no user limits). Its tables
(
user,session,account,verification) are created bynpm run db:push. - Providers activate only when their (free) OAuth credentials are set:
Google and Facebook. See
.env.examplefor the variables and the redirect URIs to register. SetBETTER_AUTH_SECRET(openssl rand -base64 32) andBETTER_AUTH_URL(your app origin) too. - Apple is deliberately left off — "Sign in with Apple" for a website needs a
paid Apple Developer account ($99/yr). Fill in
APPLE_CLIENT_ID/APPLE_CLIENT_SECRETif you ever get one and the button appears automatically.
When a recorder signs in, the sightings already captured anonymously on that
device are claimed for their account (POST /api/link), and from then on
their records are queried by account rather than by device. With no providers
configured the app simply shows no sign-in button and stays fully anonymous.
api/
index.ts the ONLY Vercel serverless entry (everything in api/ becomes
a function, so all other server code lives in server/)
server/ the Fastify application
app.ts app factory — registers routes
dev.ts local :3001 listener
db.ts Drizzle + Neon client
auth.ts Better Auth config (Google/Facebook/Apple, Drizzle adapter)
session.ts reads the signed-in user from a request
routes/ butterflies, sightings, auth mount, account linking
db/
schema.ts Drizzle tables (butterflies, sightings)
auth-schema.ts Better Auth tables (user, session, account, verification)
butterflies.ts the British butterfly reference list
seed.ts upserts species + pulls Wikimedia images
src/
lib/osgrid.ts WGS84 → OS National Grid conversion (self-contained)
hooks/ geolocation, butterflies, sightings
components/ LocationBar, ButterflyGrid, SpeciesSearch, …
App.tsx composition
- butterflies — the reference species list (name, family, image, sort order).
- reports — one recording visit:
grid_refplus rawlatitude/longitude/accuracy_m(nothing is lost), a free-textlocation_name, an anonymous per-devicerecorder_id, optional recorder name, notes, and the visit timestamp. - sightings — the butterflies within a report: species + count, plus the
sighting's own
grid_ref/latitude/longitude/accuracy_m(null falls back to the report's) andseq, the order logged (cascade-deleted with their report).
GET /api/export.csv?recorderId=… streams the recorder's sightings in the
county-recorder format (one row per butterfly per date per location): columns
Common Name, Taxon, Location, Grid Reference, Recorder, Date, Number, Life Stage, Sex, Comment, Record type, Import reference. Grid references are
space-free and as precise as the GPS fix allowed (derived from the stored
coordinates and accuracy); dates are
DD/MM/YYYY; a UTF-8 BOM keeps Excel happy. The Export CSV button lives on
the Reports tab. (The report grouping is an input convenience — one visit
holds many sightings — and flattens to this per-butterfly format on export,
each row carrying its own sighting's grid reference.)
/records is an allowlisted admin view (default emails in server/routes/admin.ts,
override with ADMIN_EMAILS). It shows every logged record on a Leaflet map
grouped by location, with a per-location list of the butterflies seen there
(species, count, date, recorder, comment), and one-click Excel (.xlsx) or
CSV downloads of the whole dataset in the county-recorder format. The gate
is enforced server-side on every /api/admin/* route; the client only shows the
link/page to allowlisted signed-in users.
- (Nothing outstanding on the record format — species, count, grid ref, date, recorder, per-butterfly comment, sex and life stage are all captured.)
Grid references are as precise as the GPS fix supports: accurate to ~10 m or better → 10-figure (1 m) ref, to ~100 m → 8-figure (10 m square), to ~1 km → 6-figure, worse → 4-figure. With no accuracy reading the ref is 8-figure. The export and the records page apply the same rule. The raw coordinates are always stored alongside, so precision can be re-derived later if needed.