A photo-first bird identification and life list tracker built on Cloudflare Pages + D1. Upload your bird photos, let AI identify the species, 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 AI handles the species identification. You just confirm with a tap.
Your photos are never stored. They're used only during identification and immediately discarded. Upload a whole day's worth of photos at once via the batch upload wizard, which clusters them into outings, identifies each bird, and lets you confirm results in one flow. Every species in your WingDex links back to the outings where you saw it, and every outing links to its species in the WingDex, so you can always cross-reference between your WingDex and your field trips.
- Privacy-first - Photos are never stored; all bird imagery comes from Wikipedia
- Batch upload - Drop a day's photos; they're auto-grouped into outings by time/GPS proximity, merged with existing sessions, and deduplicated by hash
- EXIF extraction - GPS, timestamps, and thumbnails parsed client-side
- AI species ID - GPT-4.1 vision returns ranked candidates with confidence scores and bounding boxes, grounded against ~11,000 eBird species
- WingDex Life list - First/last seen, total sightings, Wikipedia imagery; searchable and sortable
- Species detail - Hero image, Wikipedia summary, sighting history, and links to eBird / All About Birds
- Outing management - Editable locations/notes, taxonomy autocomplete, per-observation delete, eBird CSV export, Google Maps links
- eBird integration - Import/export checklists and life lists in eBird Record Format
- Dark mode - Light, dark, and system themes
- Saved locations - Bookmark birding spots with geolocation and nearby outing counts
- Dashboard - Stats, recent activity, and highlights at a glance
- Upload bird photos from your device
- EXIF GPS & timestamps are extracted and photos are clustered into outings
- Review the outing, confirm date, location (auto-geocoded), and notes
- AI identifies each bird with ranked suggestions, confidence scores, and a crop box
- Confirm, accept, mark as possible, pick an alternative, re-crop, or skip
- Saved to your WingDex with species, count, and confidence
| Layer | Technology |
|---|---|
| Platform | Cloudflare Pages + Pages Functions + D1 |
| Frontend | React 19, TypeScript, Vite 7 |
| Styling | Tailwind CSS 4, Radix UI primitives, Phosphor Icons |
| AI | GPT-4.1-mini (vision) via server-owned /api/identify-bird endpoint |
| Geocoding | OpenStreetMap Nominatim |
| Bird imagery | Wikipedia REST API |
| Testing | Vitest (unit), Playwright (e2e) |
Use Node 24+ (node --version) and install dependencies with npm ci.
git clone https://github.com/jlian/wingdex.git
cd wingdex
npm ci
npm run devnpm run dev now starts both local API runtime (wrangler pages dev on :8788) and Vite HMR (:5000) in one command.
Local auth intentionally uses two different origins depending on the flow:
localhostfor normal local web usage, passkeys, and Playwright e2e. This keeps the WebAuthn RP ID aligned with the page the browser is actually on.BETTER_AUTH_URLfor social OAuth flows that must present a hosted public callback URL to GitHub, Google, or Apple.
Operationally, that means:
- Local web on
http://localhost:5000keeps localhost semantics by default. - Hosted web on
https://localhost.wingdex.appuses the hosted domain normally. - Mobile social OAuth started through
/api/auth/mobile/startuses the hosted callback domain fromBETTER_AUTH_URL, even during local dev.
If GitHub or Google reports an invalid redirect URI during local mobile testing, verify that:
BETTER_AUTH_URLmatches the hosted callback domain registered with the provider.- The provider app allows
https://.../api/auth/callback/githubor.../googleon that hosted domain. - You are not expecting plain localhost web social OAuth to use the hosted callback domain; that path still behaves like localhost web unless you test on the hosted site.
AI calls run through the server endpoint (/api/identify-bird) via
Cloudflare AI Gateway and
require local env vars.
- Copy
.dev.vars.exampleto.dev.vars - Set
CF_ACCOUNT_IDandAI_GATEWAY_ID - Fill
OPENAI_API_KEYand (optionally)OPENAI_MODEL
CF_ACCOUNT_ID=...
AI_GATEWAY_ID=wingdex-prod
OPENAI_API_KEY=...
OPENAI_MODEL=gpt-4.1-miniOptional per-user daily limits for AI endpoints (UTC day):
AI_DAILY_LIMIT_IDENTIFY=150| Command | Description |
|---|---|
npm run dev |
Start full local dev loop: Functions API (:8788) + Vite HMR (:5000) |
npm run dev:vite |
Start Vite only |
npm run dev:cf |
Start Cloudflare Pages Functions runtime only |
npm run build |
Type-check and production build |
npm run test |
Run all tests (Vitest) |
npm run test:unit |
Run unit tests only |
npm run test:e2e |
Run end-to-end tests (Playwright) |
npm run setup:playwright |
Install Playwright Chromium + Linux deps |
npm run test:watch |
Run tests in watch mode |
npm run lint |
Lint with ESLint |
npm run preview |
Preview production build |
In Codespaces, .vscode/tasks.json runs bootstrap-workspace on folder open to bootstrap ephemeral environments. It installs Playwright dependencies and only starts npm run dev when nothing is already serving http://localhost:5000. If prompted, allow automatic tasks for this workspace.
- PR titles must follow semantic commit style (for example
feat: add outing merge UXorfix: handle wiki 404 fallback). - On push to
main, Release Please opens/updates a release PR and calculates the next semantic version:feat→ minorfix/perf/refactorand other non-breaking types → patch!orBREAKING CHANGE:→ major
- Merging the release PR updates
package.json, updatesCHANGELOG.md, and creates the Git tag/release (for examplev1.3.0).
- WingDex is designed for low-sensitivity personal birding data (outings, observations, notes).
- Data separation is enforced server-side with authenticated session user IDs on all protected
/api/*endpoints. - Production/preview persistence uses D1 (with user-scoped queries) rather than client-only key partitioning.
- In local/dev fallback mode, some flows may use browser localStorage and should be treated as development-only data storage.
- If you need strong tenant isolation for sensitive data, use a backend that enforces per-user access server-side.