Case study · Velovate
Velovate
Ride Together. Raise Together.
- .NET 10
- HotChocolate GraphQL
- EF Core
- React 19
- Flutter
- Azure Container Apps
- Service Bus + KEDA
- Terraform
- 3
- Compute tiers
- 0 → N
- Worker autoscaling
- 9
- Required CI checks
- 30 days
- Schema deprecation window
What it does
Built for teams that ride and raise
- Hierarchical team management — a chapter is a Team row with a ParentTeamId, so national → chapter structure, implicit parent membership, and cross-chapter broadcasts all fall out of one model.
- Ride scheduling + GPS tracking, with ride visibility filtered by each rider's effective team memberships.
- Per-team fundraising campaigns and silent auctions that tie training miles to charitable giving.
- Strava, RideWithGPS, and Calendar import via stored OAuth tokens, synced on a cron-triggered background job.
- Real-time live tracking, notifications, and chat over SignalR hubs pushing to connected web and mobile clients.
- One .NET GraphQL backend serving three surfaces: a React member web app and a Flutter mobile app.
Under the hood
Three tiers, one event bus
- Three-tier compute on a shared Azure Container App Environment: an always-on API (HTTP + GraphQL + SignalR), cron-triggered Jobs (0 → 1 per run), and KEDA-scaled Workers (0 → N on queue depth).
- A single Service Bus topic, velovate-events, fans events out to subscribers; each subscription filters on EventType, so a backed-up queue scales workers rather than OOM-ing the API pod.
- .NET 10 + HotChocolate GraphQL + EF Core behind shared Domain and Contracts libraries that all three tiers reference; React 19 (Apollo) and Flutter (Ferry) clients talk to the same schema.
- Multi-tier GraphQL schema evolution: fields are never removed in one step — deprecate, keep them functional for ≥30 days or 30 prod builds, and a graphql-inspector CI gate blocks breaking diffs.
- CalVer releases (YYYY.M.commits-since-the-1st) decoupled from a monotonic, never-resetting build number that backs the mobile force-update floor — the fix for a real June-1 incident where a resetting counter 426'd every client.
- Provisioned end to end with Terraform on Azure Container Apps; secrets flow from Doppler through GitHub Actions into Key Vault.
How we ship it
Shipped on a merge queue
- 1
main is gated by a GitHub merge queue that rebases each PR onto the real tip and re-runs all 9 required checks on a merge_group ref — merging only if still green, closing the green-on-stale-base gap.
- 2
Terraform applies are approval-gated: a plan classifier auto-applies safe changes but parks destroy/replace plans on a human-reviewed production environment.
- 3
Path-aware CI gating skips releases for docs-only commits and scopes deploys per app, so an API-only change never rebuilds the mobile binary — a typical single-app cycle runs ~12–15 min.
- 4
Multiple agents ship in parallel from isolated git worktrees (take-it / send-it), each deriving a collision-free dev port from the shared 3000–3999 range so concurrent stacks never fight over a port.
Ride with Velovate
Velovate is live on the App Store and Google Play — ride together, raise together.