A photo-first bird identification and life list tracker. Upload your bird photos, identify the species on your device, and build your personal WingDex over time.
WingDex is for reverse birding: people who take photos first and identify species later. Instead of checklists and field guides, you upload photos you already took, and the app identifies the birds. You just confirm with a tap.
Photos never leave your device. Identification runs entirely in the browser, or natively on iOS. There is no inference server, so there is nothing to upload a photo to.
Drop a whole day's photos at once and the batch wizard clusters them into outings by time and location, identifies each bird, and lets you confirm the lot in one pass. Species link back to the outings you saw them on, and outings link forward to the species, so you can cross-reference either direction.
- On-device species ID - ~11,000 eBird species, reranked by where and when the photo was taken
- Private by construction - photos are never uploaded or stored; all bird imagery comes from Wikipedia
- Batch upload - photos auto-grouped into outings by time and GPS, merged with existing sessions, deduplicated by hash
- Life list - first and last seen, sighting counts, searchable and sortable, with per-species detail and history
- eBird integration - import and export checklists and life lists in eBird Record Format
- Works offline - after the one-time model download, identification needs no network
- Accounts - anonymous sessions, passkeys, and GitHub / Google / Apple sign-in
- iOS app - native companion app in
ios/
- Upload photos from your device
- EXIF GPS and timestamps are read in the browser and photos are clustered into outings
- Review the outing: date, auto-geocoded location, notes
- Identify on-device, ranked by visual similarity and then by how likely each species is at that place and time of year
- Confirm the top suggestion, pick an alternative, or skip
- Saved to your WingDex
The first identification downloads about 53 MB of model files, once. After that everything is local and works offline.
WingCLIP-0.3 is a 39M-parameter image encoder distilled from BioCLIP-2 via WingCLIP-0.1. Small enough to download once and run on a phone.
Vision alone struggles to separate lookalike species, so candidates are reranked against an empirical prior over what has actually been reported in that map cell in that month. Some pairs are hard to tell apart from pixels and trivial to tell apart from geography. With that rerank the pipeline reaches 95.0 percent top-1 on a 3,322-photo held-out split.
The same model, priors, and preprocessing ship on both platforms, and a golden-vector suite pins the two implementations to each other.
| Layer | Technology |
|---|---|
| Platform | Cloudflare Workers, D1 (SQLite) |
| Frontend | React 19, TypeScript, Vite 8 |
| Styling | Tailwind CSS 4, Radix UI primitives, Phosphor Icons |
| Auth | better-auth (anonymous, passkeys, GitHub/Google/Apple OAuth) |
| Identification | WingCLIP-0.3 via ONNX Runtime Web (browser) and Core ML (iOS) |
| Geocoding | WingDex proxy backed by Geoapify (results may incorporate OpenStreetMap data) |
| Bird imagery | Wikipedia REST API |
| Testing | Vitest (unit), Playwright (e2e), XCTest (iOS) |
| iOS | Swift, XcodeGen |
Requires Node 24+. No API keys: identification runs on the device, and anonymous auth works out of the box.
git clone https://github.com/jlian/wingdex.git
cd wingdex
npm install
npm run db:migrate
npm run devnpm run dev serves Vite on :5000 with Wrangler behind /api/* on :8787, and creates .dev.vars from the example on first run. npm stop clears stale ports. Local D1 state lives in ~/.cache/wingdex/wrangler-state.
Run npm run check (lint, typecheck, unit) before pushing, and npm run check:all (adds e2e and a production build) when the change touches functions/, e2e/, routing, auth, or data flow. Everything runnable is in package.json under scripts.
| Path | Purpose |
|---|---|
src/components/ |
React components: ui/ primitives, pages/, flows/ |
src/lib/ |
Client-side logic, including identification and ranking |
src/__tests__/ |
Vitest unit and integration tests |
functions/ |
Cloudflare Workers API routes and shared server logic |
migrations/ |
D1 SQL migrations |
e2e/ |
Playwright specs |
ios/ |
Native iOS app (XcodeGen) |
ml/ |
Model conversion and cross-platform parity harnesses |
PR titles follow Conventional Commits (feat: add outing merge UX, fix: handle wiki 404 fallback). On merge to main, Release Please works out the next version, updates CHANGELOG.md, and tags it.
See CONTRIBUTING.md.