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.
Three-tier compute on a shared Azure Container App Environment: an always-on API publishes domain events to a single Service Bus topic, fanned out to cron-triggered Jobs and KEDA-scaled Workers over a shared EF Core domain.

How we ship it

Shipped on a merge queue

  1. 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. 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. 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. 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.