Rewards urban commuters with real discount coupons for walking, cycling, and using public transit. Built as an end-to-end production project — Android app + FastAPI backend + CI/CD — by a solo developer.
GreenStride is a full-stack Android application that:
- Tracks eco-friendly commutes (walking, cycling, public transit) via GPS speed analysis
- Awards GreenCoins per 100 m of verified eco travel, with streak bonuses, daily challenges, and a spin wheel
- Redeems coins for brand coupons (Zalando, Decathlon, Myntra, Nykaa…) via Awin / Admitad / VCommission affiliate networks
- Saves coupons to Google Wallet with a single tap
- Operates in two markets — EU (Germany-first) and India — with auto-detected regional coupon catalogues
Live landing page: greenstride-app.netlify.app
Live API: com-greenstride-android.fly.dev/health
┌──────────────────────────────────────────────────────────────────┐
│ Android App (Kotlin) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ UI Layer │ │ Domain Layer │ │ Data Layer │ │
│ │ Compose │◄──│ Use Cases │◄──│ Repositories │ │
│ │ ViewModels │ │ Domain Models│ │ RemoteDataSource │ │
│ └──────────────┘ └──────────────┘ │ (Retrofit + OkHttp) │ │
│ └──────────┬───────────┘ │
└────────────────────────────────────────────────────┼─────────────┘
│ HTTPS/JWT
┌────────────────────────────────────────────────────▼─────────────┐
│ FastAPI Backend (Python 3.12) │
│ │
│ /v1/auth /v1/me /v1/challenges /v1/trips /spin │
│ /referral /coupons /offsets /admin /postback │
│ │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────┐ │
│ │ SQLAlchemy │ │ Fraud Engine │ │ Affiliate Sync │ │
│ │ ORM + SQLite│ │ (GPS analysis)│ │ Awin · Admitad · VC │ │
│ └──────────────┘ └───────────────┘ └──────────────────────┘ │
│ │
│ Deployed on Fly.io (Frankfurt) · Auto-deployed via GitHub Actions │
└───────────────────────────────────────────────────────────────────┘
| Layer | Pattern | Key files |
|---|---|---|
| UI | Jetpack Compose + ViewModel | HomeScreen, WalletScreen, ChallengesScreen |
| Domain | Use cases + Repository interfaces | GetProfileUseCase, RedeemCouponUseCase |
| Data | Repository impls + DTOs | ApiRemoteDataSource, GreenStrideApi |
| DI | Hilt (SingletonComponent) | NetworkModule, RepositoryModule |
| Auth | JWT stored in SessionManager + AuthInterceptor |
Injected into OkHttp chain |
| Component | Technology | Responsibility |
|---|---|---|
| API framework | FastAPI + Pydantic v2 | Request validation, OpenAPI docs |
| ORM | SQLAlchemy 2.0 (DeclarativeBase) |
All DB access |
| Auth | python-jose JWT |
Device-hash login, Bearer tokens |
| Scheduler | APScheduler | Awin/Admitad/VCommission coupon sync every 6 h |
| Fraud engine | Pure Python | GPS speed validation server-side |
| Deployment | Fly.io (shared-cpu-1x, Frankfurt) |
Auto-deploy on backend/** push |
The most critical system in the app — prevents users from earning coins by driving a car or faking trips. Every trip is re-validated on the server regardless of what the client reports.
# backend/services/fraud_detection.py (excerpt)
_PROFILES: dict[str, _ModeProfile] = {
"WALKING": _ModeProfile(co2_per_km=0.21, coins_per_100m=2, max_avg_kmh=8.0, max_peak_kmh=15.0),
"CYCLING": _ModeProfile(co2_per_km=0.21, coins_per_100m=4, max_avg_kmh=40.0, max_peak_kmh=60.0),
"UBAHN_SBAHN": _ModeProfile(co2_per_km=0.09, coins_per_100m=3, max_avg_kmh=150.0, max_peak_kmh=200.0),
"BUS_TRAM": _ModeProfile(co2_per_km=0.09, coins_per_100m=2, max_avg_kmh=70.0, max_peak_kmh=100.0),
}
# Five independent signals checked per trip:
# Signal 1 — mode-specific peak speed cap
# Signal 2 — rolling-average speed cap
# Signal 3 — GPS-zero ratio (U-Bahn underground confirmation)
# Signal 4 — distance sanity (implied vs declared within 30%)
# Signal 5 — sample count vs duration (tamper detection)Why per-mode profiles? An early version used a single 50 km/h ceiling, which flagged legitimate S-Bahn trips (60–120 km/h) as fraud. The refactor to per-mode profiles eliminated false positives entirely.
Users register with a SHA-256 hash of their device ID — no email, no name, nothing reversible to a real person. This satisfies GDPR Art. 5(1)(c) data minimisation from first contact.
// AuthInterceptor.kt
override fun intercept(chain: Interceptor.Chain): Response {
val token = sessionManager.getToken() ?: return chain.proceed(chain.request())
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}Three affiliate networks feed the coupon catalogue via scheduled background jobs. The Android app always reads from the backend's unified catalogue — network-specific API differences are invisible to the client.
Awin EU sync ──┐
Awin IN sync ──┼──► APScheduler (every 6h) ──► coupon_catalogue table ──► /v1/me/wallet
VCommission ──┤
Admitad ──┘
Switching from mock to real data source required changing one line in NetworkModule.kt:
// NetworkModule.kt
@Provides @Singleton
fun provideRemoteDataSource(api: ApiRemoteDataSource): RemoteDataSource = api
// was: fun provideRemoteDataSource(mock: MockRemoteDataSource): RemoteDataSource = mockAll ViewModels, use cases, and repositories were unaffected — the interface boundary absorbed the change completely.
| Feature | Android | Backend |
|---|---|---|
| GPS trip tracking & auto mode detection | ✅ | ✅ Server validates |
| Coin ledger with streak multipliers | ✅ | ✅ |
| 5 user levels (Green Starter → Climate Champion) | ✅ | ✅ |
| 13 challenges (personal / city / employer) | ✅ | ✅ |
| Daily spin wheel | ✅ | ✅ |
| Referral programme | ✅ | ✅ |
| Coupon wallet (Awin + Admitad + VCommission) | ✅ | ✅ |
| Google Wallet integration | ✅ | ✅ Signed JWT |
| GDPR Art. 17 right-to-erasure | ✅ In-app | ✅ Grace-period deletion |
| CCPA §1798.120 opt-out | ✅ | ✅ |
| Consent audit log | — | ✅ |
| Firebase Crashlytics + Analytics | ✅ | — |
| GitHub Actions CI (unit tests + JaCoCo) | ✅ | — |
| GitHub Actions CD (Fly.io auto-deploy) | — | ✅ |
| Play Store release pipeline (Fastlane) | ✅ | — |
- Language: Kotlin
- UI: Jetpack Compose + Material 3
- Architecture: Clean Architecture (UI → Domain → Data)
- DI: Hilt
- Async: Kotlin Coroutines + StateFlow
- Networking: Retrofit 2 + OkHttp + Gson
- Local storage: DataStore (preferences)
- Analytics: Firebase Analytics + Crashlytics
- Build: Gradle Kotlin DSL,
local.propertiessecrets with CI env-var fallback
- Language: Python 3.12
- Framework: FastAPI 0.115 + Pydantic v2
- ORM: SQLAlchemy 2.0 (
DeclarativeBase) - Auth:
python-joseJWT + bcrypt device-hash - Scheduler: APScheduler (async)
- HTTP client:
aiohttp(affiliate sync) - DB: SQLite (dev/staging) · PostgreSQL-ready
- Deployment: Fly.io (Frankfurt) · Docker multi-stage build
- CI/CD: GitHub Actions (deploy on
backend/**push)
| File | What it shows |
|---|---|
GreenStrideApi.kt |
Clean Retrofit interface covering all 15 backend endpoints |
NetworkModule.kt |
Hilt DI wiring — interceptor chain, Retrofit, RemoteDataSource binding |
fraud_detection.py |
Multi-signal GPS fraud engine with per-mode speed profiles |
me.py |
Android-compatible API wrapper — maps DB schema to Android DTO schema |
challenges.py |
Challenge progress engine — personal, city, and employer challenge types |
Push to master (backend/**)
│
▼
GitHub Actions: backend-deploy.yml
│ flyctl deploy --remote-only
▼
Fly.io builds Docker image (python:3.12-slim, multi-stage)
│
▼
Machine starts · lifespan() runs:
create_all() → column migrations → seed challenges → start scheduler
│
▼
Health check passes → traffic routed
Push to release/x.y.z branch
│
▼
GitHub Actions: deploy.yml
1. Unit tests (gate) — ./gradlew testDebugUnitTest
2. Fastlane: build signed AAB → upload to Play Store
3. Tag release vX.Y.Z
- GDPR Art. 5(1)(c) — data minimisation: device hash only, no PII at signup
- GDPR Art. 7 — explicit consent before affiliate tracking activates
- GDPR Art. 17 — in-app erasure request: immediate deactivation, hard-delete in 30 days
- GDPR Art. 20 — data export available in Settings
- CCPA §1798.120 — opt-out of sale; no personal data sold
- GPS raw samples never leave the device — only derived speed summary is sent
| Onboarding | Home / Dashboard | Trip Tracking |
![]() |
![]() |
![]() |
| Challenges | Coupon Wallet | Profile & Impact |
![]() |
![]() |
![]() |
This is a showcase repository. Source code is available to recruiters and hiring managers on request — please open a GitHub issue or email hello@greenstride.app.





