A full-stack, gamified hydration tracker built for Android-first usage with Expo Go. The app calculates a personalised daily water target based on real-time weather, humidity, and physical activity, then makes hitting that target rewarding through a plant-growing progression system, XP, badges, and a Botanical Garden album.
- Overview
- How the Target Is Calculated
- Gamification System
- Feature List
- Tech Stack
- Project Structure
- Quick Start
- Environment Variables
- API Reference
- Reminder Architecture
- Expo Go Constraints
Intelligent Hydration (branded in-app as HYDRA) adapts your water goal to conditions you actually live in — not a flat 8-glasses-a-day guess. On a 42°C Dubai afternoon after a workout, your target could reach 3,300mL. On a mild 25°C day with no activity it stays near the 2,000mL baseline.
The app is local-first: all calculations and logging work immediately without a network connection. When the backend is reachable, logs sync to Supabase and the server schedules adaptive push reminders.
Daily Target (mL) = 2,000 + weather_bonus + humidity_bonus + activity_bonus
| Factor | Condition | Added |
|---|---|---|
| Weather | Temperature ≥ 40°C | +25mL per degree above 30°C |
| Humidity | Relative humidity > 70% | +150mL flat |
| Steps | Per 1,500 steps | +150mL |
| Workout | Per 10 intense minutes | +150mL |
| Resilient plant | Fernie / Ghaf active | ×0.9 multiplier on total |
Daily cap: Target × 1.5 — the app warns you when you are about to exceed it.
Temperature 42°C, humidity 68%, 4,500 steps, 0 workout minutes:
Weather: (42 - 30) × 25 = 300mL
Humidity: 0 (68% < 70%)
Activity: floor(4500 / 1500) × 150 = 450mL
──────────────────────────────────────
Target: 2,000 + 300 + 0 + 450 = 2,750mL
Cap: 2,750 × 1.5 = 4,125mL
- Every 250mL logged earns 10 XP
- When temperature > 35°C, the Sun Oasis multiplier applies: ×1.5 XP
- Level =
floor(total XP / 100) + 1
Each week a plant is active. Growth percentage (0–100%) resets every Sunday:
| Event | Growth gained |
|---|---|
| Hit daily target (normal) | +15% |
| Hit daily target, temp > 38°C (Solar Flare Boost) | +25% |
At 100% growth the plant is harvested into the Botanical Garden album (8 slots). A new plant starts the following week.
| # | Name | Nickname | Type | Bonus |
|---|---|---|---|---|
| 1 | Sahara Cactus | Spike | Desert | +25% growth boost on days > 35°C |
| 2 | Lucky Bamboo | Coco | Tropical | Steady growth in high humidity |
| 3 | Desert Rose | Rosie | Bloom | Balanced heat-season growth |
| 4 | Emirati Ghaf Tree | Ghaf | Heritage | Requires 10% less water to reach full growth |
At onboarding users pick a starter plant (Spike, Coco, or Fernie the Fern) which begins their first week.
| Badge | Unlock condition |
|---|---|
| First Sprout | Reach Level 2 |
| Hydra Streak | Hit the daily target 3 days in a row |
| Desert Bloom | Log water when temperature > 35°C |
| Smart Shopper | Log 3 drinks using a named grocery brand |
| Water Score | Log 5 drinks in a single session |
- Dynamic daily water target recalculated on every state change
- Real-time progress visualised as a water-filling animated ring
- Quick log buttons — Sip (100mL), Cup (250mL), Bottle (500mL)
- Custom containers — name and save any bottle size (e.g. "Mango Bottle · 1,600mL") and log with one tap
- Timestamped log history with delete support
- Daily cap warning when intake exceeds 1.5× the target
- One-time GPS location request at onboarding
- Weather fetched via the backend proxy to OpenWeatherMap (lat/lon)
- Fallback: Dubai baseline (42°C, 65% humidity) when offline or permission denied
- Region auto-detected (AE / GB / US) to set the correct grocery market
- Localised brand catalogue by region:
- Dubai/Sharjah (AED): Al Ain, Masafi
- London (GBP): Evian
- New York (USD): Poland Spring
- Mineral profile per brand: Calcium, Sodium, Magnesium, pH
- Tap a brand to log a drink and attribute it to that product
- Session totals: spend, calcium, sodium, magnesium logged
- XP accumulation with Sun Oasis weather multiplier
- Level display with XP-to-next-level progress bar
- Streak counter for consecutive days hitting the target
- 5-badge system with earned/locked visual states
- Botanical Garden shelf — 8 slots showing harvested plant emojis
- Frontend registers an Expo push token
- Backend schedules reminders via APScheduler:
- Normal conditions: every 2 hours
- Extreme heat (> 38°C): every 45 minutes
- Delivered through the Expo Push API
- Email + password registration and login
- JWT access token (30-minute expiry) + refresh token (7-day expiry)
- On login, XP, streak, badges, today's logs, and user preferences are restored from the backend
| Layer | Technology |
|---|---|
| Mobile frontend | React Native 0.85, Expo SDK 56, React 19 |
| Backend API | FastAPI (Python 3.10+) |
| Database | Supabase (PostgreSQL) |
| Authentication | JWT — python-jose, bcrypt |
| Scheduling | APScheduler 3.10 |
| Push notifications | Expo Push API |
| Weather | OpenWeatherMap REST API |
| Icons | @expo/vector-icons (Ionicons) |
IntelligentHydration/
│
├── App.js # Entire frontend — state, screens, components, styles
├── index.js # Expo root component registration
├── app.json # Expo config (slug, permissions, icons)
├── plantsCatalog.js # Plant definitions and weekly rotation logic
├── package.json
│
├── assets/ # App icon, splash, adaptive icon layers
│
└── backend/
├── main.py # FastAPI app — mounts all routers, starts APScheduler
├── config.py # Pydantic settings loaded from .env
├── db.py # Supabase client
├── auth_utils.py # JWT creation/validation, bcrypt helpers
├── weather_service.py # OpenWeatherMap wrapper
│
├── routers/
│ ├── auth.py # Register, login, token refresh, push-token
│ ├── hydration.py # Log drink, get history, delete log, daily target, activity
│ ├── gamification.py # XP state, award XP
│ ├── weather.py # Weather proxy (lat/lon → current conditions)
│ ├── economics.py # Brand catalogue by region, session summary
│ ├── user.py # Profile, push token storage
│ └── notifications.py# Schedule reminder jobs
│
├── models/
│ └── schemas.py # Pydantic request/response models
│
├── tasks/
│ └── reminder_scheduler.py # APScheduler job definitions
│
├── requirements.txt
├── .env.example
└── .env # Not committed — add your keys here
- Node.js 18+
- Python 3.10+
- Expo Go installed on your Android or iOS device
- A free Supabase project
- A free OpenWeatherMap API key
npm install
npx expo startScan the QR code with Expo Go. The app works fully offline in demo mode — the backend is optional.
cd backend
# Create and activate virtual environment
python -m venv .venv
# Windows
.venv\Scripts\Activate.ps1
# macOS / Linux
source .venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload --host 0.0.0.0 --port 8000In App.js, update the IP address on line 31 to your machine's local IP:
const API_URL = Platform.OS === 'web'
? 'http://localhost:8000'
: 'http://YOUR_LOCAL_IP:8000'; // ← replace thisFind your IP with ipconfig (Windows) or ifconfig (Mac/Linux). Both your phone and laptop must be on the same Wi-Fi network.
Copy backend/.env.example to backend/.env and fill in:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
OPENWEATHERMAP_API_KEY=your-owm-key
JWT_SECRET=any-long-random-string
ENVIRONMENT=developmentAll endpoints are prefixed with the base URL (e.g. http://localhost:8000).
Protected endpoints require the header Authorization: Bearer <access_token>.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register |
— | Create account; returns JWT pair |
| POST | /auth/login |
— | Verify credentials; returns JWT pair |
| POST | /auth/refresh |
— | Exchange refresh token for new access token |
| POST | /auth/push-token |
✓ | Store Expo push token |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /user/profile |
✓ | Return profile and location |
| POST | /user/push-token |
✓ | Update push token on profile |
| GET | /user/preferences |
✓ | Load saved body metrics and reminder settings |
| POST | /user/preferences |
✓ | Save weight, activity level, custom target, reminder window |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /log/hydration |
✓ | Record a drink (volume_ml, optional brand_id) |
| GET | /log/history |
✓ | Last 100 hydration entries |
| DELETE | /log/hydration/{log_id} |
✓ | Remove a log entry |
| GET | /target/daily |
✓ | Calculated target for today (weather + activity) |
| POST | /activity/manual |
✓ | Update steps and workout_minutes |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /weather/current?lat=&lon= |
— | Current conditions via OpenWeatherMap proxy |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /gamification/state |
✓ | XP, level, streak, badges, plant growth |
| POST | /gamification/xp |
✓ | Award XP for a logged drink |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /brands/local?region=AE |
— | Brands for a region (AE, GB, US) |
| GET | /economics/summary |
✓ | Session spend and mineral totals |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /notifications/schedule |
✓ | Register push token with the reminder scheduler |
Recurring reminders cannot be scheduled locally inside Expo Go (no background workers). Instead:
- On first load the app calls
Notifications.getExpoPushTokenAsync()and sends the token to the backend viaPOST /auth/push-token. - The backend's APScheduler picks up the token and registers a job:
- Normal (temp ≤ 38°C): fires every 2 hours
- Extreme heat (temp > 38°C): fires every 45 minutes
- Each job fires a POST to
https://exp.host/--/api/v2/push/sendwith the user's token. Expo delivers the notification even when the app is closed.
The app is intentionally kept in Expo Go managed workflow — no prebuild, no eject.
| Capability | Status | Notes |
|---|---|---|
| Android & iOS | ✅ | Scan QR code with Expo Go |
| GPS location | ✅ | Foreground permission only |
| Push notifications | ✅ | Via Expo Push API (server-side scheduling) |
| Local background workers | ❌ | APScheduler on the FastAPI backend handles this |
| Native modules | ❌ | Not used — stays compatible with Expo Go |