A web portal for tracking bird observations from eBird CSV exports. Built for the Nomadic Big Year 2026 competition for full-time RVers.
- Magic Link Authentication - Passwordless email-only login
- CSV Upload - Import eBird CSV exports (filters to 2026 observations only)
- Automatic Leaderboard - Rankings by species count with tiebreaker
- Privacy Controls - Choose between public, counts-only, or private
- User Profiles - View stats, observations, and geographic coverage
- Deduplication - Prevents duplicate observations on re-upload
- FastAPI - Python web framework
- PostgreSQL - Database with materialized views for performance
- SQLAlchemy - ORM for database models
- SendGrid - Email service for magic links
- Pandas - CSV parsing
- JWT - Authentication tokens
- React 18 - UI library
- Vite - Build tool and dev server
- React Router - Client-side routing
- Axios - HTTP client
nomadic-big-year/
├── backend/
│ ├── main.py # FastAPI application entry point
│ ├── database.py # Database connection and session
│ ├── models.py # SQLAlchemy ORM models
│ ├── schemas.py # Pydantic request/response schemas
│ ├── auth.py # Authentication utilities (magic link, JWT)
│ ├── csv_parser.py # eBird CSV parsing logic
│ ├── routers/
│ │ ├── auth.py # Auth endpoints
│ │ ├── upload.py # CSV upload endpoints
│ │ ├── leaderboard.py # Leaderboard endpoints
│ │ └── user.py # User profile endpoints
│ ├── requirements.txt # Python dependencies
│ └── .env.example # Environment variables template
├── frontend/
│ ├── src/
│ │ ├── main.jsx # React entry point
│ │ ├── App.jsx # Main app component with routing
│ │ ├── pages/ # Page components
│ │ │ ├── HomePage.jsx
│ │ │ ├── LoginPage.jsx
│ │ │ ├── VerifyPage.jsx
│ │ │ ├── LeaderboardPage.jsx
│ │ │ ├── UploadPage.jsx
│ │ │ └── ProfilePage.jsx
│ │ └── services/ # API and auth services
│ │ ├── api.js
│ │ └── auth.js
│ ├── package.json # Node dependencies
│ ├── vite.config.js # Vite configuration
│ └── index.html # HTML template
├── database/
│ └── schema.sql # PostgreSQL schema with functions
└── README.md
- Python 3.10+
- Node.js 18+
- PostgreSQL 12+
- SendGrid account (for production) or use dev mode (prints links to console)
Create a PostgreSQL database:
createdb nomadic_big_yearRun the schema SQL:
psql nomadic_big_year < database/schema.sqlNavigate to backend directory:
cd backendCreate a virtual environment:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activateInstall dependencies:
pip install -r requirements.txtCreate .env file (copy from .env.example):
cp .env.example .envEdit .env with your configuration:
DATABASE_URL=postgresql://user:password@localhost:5432/nomadic_big_year
JWT_SECRET_KEY=your-random-32-byte-secret-key
FRONTEND_URL=http://localhost:5173
SENDGRID_API_KEY=your-sendgrid-api-key # Optional for dev
FROM_EMAIL=noreply@nomadicbigyear.com
PORT=8000Generate JWT secret key:
python -c "import secrets; print(secrets.token_urlsafe(32))"Run the backend:
python main.pyThe API will be available at http://localhost:8000
API docs at http://localhost:8000/docs
Navigate to frontend directory:
cd frontendInstall dependencies:
npm installCreate .env file:
echo "VITE_API_URL=http://localhost:8000" > .envRun the frontend:
npm run devThe app will be available at http://localhost:5173
-
Sign Up/Login
- Go to the app and click "Log In"
- Enter your email address
- Check your email for the magic link
- Click the link to log in (expires in 15 minutes)
-
Export eBird Data
- Go to ebird.org/downloadMyData
- Request your data download
- Check email for download link
- Download and extract the ZIP file
- Locate
MyEBirdData.csv
-
Upload CSV
- Click "Upload CSV" in the nav
- Drag and drop or browse for your CSV file
- Click "Upload CSV"
- View import statistics
- Only 2026 observations are imported
- Duplicates are automatically skipped on re-upload
-
View Leaderboard
- Rankings by species count for 2026
- Ties broken by most recent observation date
- See participant privacy levels
-
Manage Profile
- Edit your name
- Change privacy settings:
- Public: Full observation details visible
- Counts Only: Only species count visible
- Private: Don't appear on leaderboard
- View your statistics
- Public (default): Appear on leaderboard with species count. Other logged-in users can view your observation details.
- Counts Only: Appear on leaderboard with species count. Observation details are hidden from others.
- Private: Don't appear on leaderboard at all. Only you can see your data.
- Only observations from 2026 are imported
- Deduplication based on
(user_id, submission_id, scientific_name)unique constraint - Re-uploading same CSV will skip duplicates automatically
- Historical data (2022-2025) is filtered out during import
- Maximum file size: 10MB
- Format: Must be the standard eBird CSV export with 23 columns
- Create new project on Railway
- Add PostgreSQL service
- Add Python service:
- Connect GitHub repo
- Root directory:
backend - Build command:
pip install -r requirements.txt - Start command:
uvicorn main:app --host 0.0.0.0 --port $PORT
- Set environment variables:
DATABASE_URL(auto-set by Railway)JWT_SECRET_KEYFRONTEND_URL(your Vercel URL)SENDGRID_API_KEYFROM_EMAIL
- Deploy
- Import GitHub repo to Vercel
- Framework preset: Vite
- Root directory:
frontend - Build command:
npm run build - Output directory:
dist - Environment variable:
VITE_API_URL= your Railway backend URL
- Deploy
Run the schema SQL on your production database:
psql $DATABASE_URL < database/schema.sqlPOST /auth/request-magic-link- Request magic link emailGET /auth/verify?token={token}- Verify magic link and get JWTPOST /auth/logout- Logout (client-side)
POST /upload/csv- Upload eBird CSV file (requires auth)
GET /leaderboard?year=2026&limit=100- Get leaderboardGET /leaderboard/{user_id}/progress?year=2026- Get monthly progress
GET /user/me- Get current user profile with statsPATCH /user/me- Update profile (name, privacy)GET /user/me/observations- Get observation list with paginationGET /user/me/geographic-stats- Get states/counties visited
GET /health- Check API and database health
- users - Participant accounts, magic link tokens, privacy settings
- observations - All eBird observations (23 columns from CSV)
- monthly_stats - Monthly species count progression per user
- geographic_stats - States/counties visited per user
- species_summary - Leaderboard view with species counts, last observation dates
refresh_species_summary()- Refresh materialized viewcalculate_monthly_stats(user_id, year)- Recalculate monthly statscalculate_geographic_stats(user_id)- Recalculate geographic stats
Backend tests (TODO):
cd backend
pytestBackend:
- Follow PEP 8
- Use type hints
- Document functions with docstrings
Frontend:
- Use functional components with hooks
- Keep components small and focused
- Use meaningful variable names
Database connection fails:
- Check
DATABASE_URLin.env - Ensure PostgreSQL is running
- Verify database exists:
psql -l
Magic link emails not sending:
- Check
SENDGRID_API_KEYis set - In dev mode, links are printed to console
- Check SendGrid dashboard for errors
CSV upload fails:
- Ensure file is valid eBird CSV export
- Check file size (max 10MB)
- Verify 23 required columns exist
- Check backend logs for parsing errors
API calls fail:
- Check
VITE_API_URLin.env - Verify backend is running
- Check browser console for CORS errors
- Ensure JWT token is in localStorage
Login doesn't work:
- Check magic link hasn't expired (15 min)
- Verify email service is working
- Check browser allows localStorage
- Try clearing localStorage and retry
Phase 2 - Analytics:
- Monthly progress charts (Recharts)
- Geographic coverage map (Leaflet)
- Recent activity feed
- Species comparison tool
Phase 3 - Features:
- Rare bird alerts
- Photo uploads (ML Catalog integration)
- Export leaderboard as image for Facebook
- Email notifications (weekly digest)
- County-level tracking
- Life list vs Big Year toggle
- Search observations
- Filter leaderboard by state
This is a private project for the Nomadic Big Year 2026 RV community.
For technical issues or questions, post in the Facebook group or contact the challenge organizer.
Private project - All rights reserved.
Good luck and happy birding! 🦅 🚐
Let's see who can spot the most birds while exploring this beautiful country in 2026!