Force Update in Flutter (Part 7): Custom API with Dart Shelf
Flutter Development
9 min read
July 15, 2026

Force Update in Flutter (Part 7): Custom API with Dart Shelf

Part 7 — own required_version with a tiny Dart Shelf endpoint, multi-env URLs, and production deploy notes.

Muhammad Nabi Rahmani

Muhammad Nabi Rahmani

Flutter Developer passionate about creating beautiful mobile experiences

Force Update in Flutter (Part 7): Custom API with Dart Shelf

Force Update series: 1. Why you need it · 2. Upgrader · 3. Remote config intro · 4. force_update_helper · 5. GitHub Gist · 6. Firebase Remote Config · 7. Dart Shelf API

Gist is fine for small apps. Firebase is fine when you already pay the Firebase tax. When you have (or want) your own backend, a single endpoint is the cleanest source of truth:

GET /required_version
→ 200 text/plain
1.4.2

This part uses Dart Shelf so the whole stack stays in Dart — same language as the Flutter client.

Why a custom endpoint

  • No Gist rate limits
  • No Firebase if you do not need it
  • Easy multi-env (dev / stg / prod) with different floors
  • Can later expand to JSON: soft version, messages, store URLs, kill switches
  • Fits existing auth, logging, and deploy pipelines

Minimal Shelf server

import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';

void main() async {
  final app = Router();

  app.get('/required_version', (Request request) {
    final version = Platform.environment['REQUIRED_VERSION'] ?? '1.0.0';
    return Response.ok(
      version,
      headers: {'content-type': 'text/plain; charset=utf-8'},
    );
  });

  final handler = Pipeline()
      .addMiddleware(logRequests())
      .addHandler(app.call);

  final port = int.parse(Platform.environment['PORT'] ?? '8080');
  final server = await shelf_io.serve(handler, InternetAddress.anyIPv4, port);
  print('Listening on ${server.address.host}:${server.port}');
}

Flutter client

ForceUpdateClient(
  fetchRequiredVersion: () async {
    // TODO: production URL from env / flavor
    const baseUrl = String.fromEnvironment(
      'API_BASE_URL',
      defaultValue: 'http://10.0.2.2:8080', // Android emulator → host
    );
    final response = await dio.get('$baseUrl/required_version');
    return (response.data as String).trim();
  },
  iosAppStoreId: Env.appStoreId,
);

Notes:

  • iOS simulator often uses http://127.0.0.1:8080
  • Android emulator uses 10.0.2.2 for host loopback
  • Real devices need your LAN IP or a deployed HTTPS URL
  • Cleartext HTTP needs network security config / ATS exceptions — prefer HTTPS in prod

Flavors map to base URLs

FlavorBase URL example
devhttps://dev-api.your-app.com
stghttps://stg-api.your-app.com
prodhttps://api.your-app.com

Each environment can set REQUIRED_VERSION independently so QA can force-update staging without bricking production.

Deploy options (pick one)

  • Globe / edge Dart hosts — designed for Dart servers
  • Docker → Cloud Run, ECS, Azure Container Apps
  • VM / VPS — simple dart run or AOT binary behind Caddy/Nginx
  • Existing backend — skip Shelf and add GET /required_version to Nest, Rails, Go, etc. The Flutter side does not care

Beyond this course-length note: deploying Dart backends style guides exist; the rule is HTTPS + env vars + health checks.

Growing the contract

Start with plain text. When product needs more:

{
  "required_version": "1.4.0",
  "recommended_version": "1.5.1",
  "message": "This version can no longer sync. Please update.",
  "force": true
}

Keep the client tolerant: if JSON parse fails, fall back to safe defaults.

Production checklist

  • HTTPS only in prod
  • REQUIRED_VERSION set per environment
  • Flutter flavor points at correct base URL
  • Caching + timeout on the client
  • Raise version only after store rollout
  • Logging/metrics on the endpoint

Series wrap-up

PartToolBest for
1ConceptsPolicy & soft vs hard
2upgraderAlways match store
3Remote min versionKill switch design
4force_update_helperClient glue
5GistZero backend
6Firebase RCExisting Firebase apps
7Shelf / APIOwn backend, multi-env

My default for shipping apps: remote min version + helper + API or Firebase, hard only when it matters, Shorebird for Dart-only fire drills.

← Previous Part 6: Firebase Remote Config
Series start → Part 1: Why force update

Share:

Keep Reading

More articles you might enjoy