Two values drive every request: the AppEngine base URL and the orgid header. Configure them once at app start and forget about them — the rest of the SDK reads from a single source.
Environment file
The appmint_mobile reference app keeps environment selection in lib/config/environment.dart and switches automatically based on Flutter's build mode. Copy this file into your project and edit the URLs as needed.
// lib/config/environment.dart
enum Environment { development, production }
class EnvironmentConfig {
static Environment _currentEnvironment = Environment.development;
static Environment get currentEnvironment => _currentEnvironment;
static void setEnvironment(Environment env) => _currentEnvironment = env;
static bool get isDevelopment => _currentEnvironment == Environment.development;
static bool get isProduction => _currentEnvironment == Environment.production;
static String get appengineEndpoint {
switch (_currentEnvironment) {
case Environment.development:
return 'http://localhost:3300';
case Environment.production:
return 'https://appengine.appmint.io';
}
}
// Default org for dev only — production users must enter their own.
static String get defaultOrgId => isDevelopment ? 'demo' : '';
// Network
static int get connectionTimeout => 30000;
static int get receiveTimeout => 30000;
static int get maxRetries => 3;
// Feature flags
static bool get enableDebugLogs => isDevelopment;
static bool get enableCrashReporting => isProduction;
// Storage keys
static const String accessTokenKey = 'access_token';
static const String refreshTokenKey = 'refresh_token';
static const String userDataKey = 'user_data';
static const String orgIdKey = 'org_id';
// Pagination
static const int defaultPageSize = 20;
}
Auto-switch on build mode
Wire the environment in main.dart so debug builds use development and release builds use production.
// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'config/environment.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (kReleaseMode) {
EnvironmentConfig.setEnvironment(Environment.production);
} else {
EnvironmentConfig.setEnvironment(Environment.development);
}
if (kDebugMode) {
debugPrint('AppEngine: ${EnvironmentConfig.appengineEndpoint}');
debugPrint('Default orgId: ${EnvironmentConfig.defaultOrgId}');
}
runApp(const MyApp());
}
flutter run will automatically use development. flutter build apk --release or flutter build ios --release will use production.
Where the orgId comes from
orgid identifies the tenant on every request. There are three common ways to populate it.
1. Hard-coded for single-tenant apps
If your app ships to one tenant only — a private operator app, or a white-label build — bake the orgId into a build flavor.
// lib/config/build_config.dart
class BuildConfig {
static const String orgId = String.fromEnvironment(
'ORG_ID',
defaultValue: 'demo',
);
}
Build with flutter build ios --dart-define=ORG_ID=acme-prod.
2. Entered by the user on first launch
Most consumer apps prompt the user once on the sign-in screen, then persist the value in shared_preferences.
final prefs = await SharedPreferences.getInstance();
String? orgId = prefs.getString(EnvironmentConfig.orgIdKey);
orgId ??= EnvironmentConfig.defaultOrgId;
if (orgId.isEmpty) {
// Show "Enter your workspace" screen
}
3. Resolved from an email domain
If users sign in with their work email, ask AppEngine which org they belong to via GET /profile/who-is/{email} (the lookup is public). This avoids ever asking the user for an orgId.
The orgid header is lowercase. The customer endpoint also accepts x-org-id, but orgid is the canonical name and what every reference app uses.
Headers AppEngine expects
Every authenticated request sends three headers. Get them right once in your HTTP client and forget about them everywhere else.
Content-Type: application/json
Authorization: Bearer <jwt>
orgid: <your-org-id>
Content-Type is omitted on GET and DELETE. Authorization is omitted on public endpoints (sign-in, sign-up, password reset). orgid is always required — even on public endpoints — because the tenant boundary is enforced server-side.
If you forget orgid, AppEngine returns 400. If you send the wrong one, it returns 200 with empty results — you have just queried somebody else's tenant.
Local development against localhost:3300
The development AppEngine server runs on http://localhost:3300 by default. From a simulator/emulator the URL changes:
| Target | Base URL |
|---|---|
| iOS simulator | http://localhost:3300 |
| Android emulator | http://10.0.2.2:3300 |
| Physical device on LAN | http://<host-lan-ip>:3300 |
The reference apps detect the platform and adjust:
import 'dart:io' show Platform;
static String get appengineEndpoint {
if (isProduction) return 'https://appengine.appmint.io';
if (Platform.isAndroid) return 'http://10.0.2.2:3300';
return 'http://localhost:3300';
}
If you are running AppEngine on your LAN and testing on a phone, set appengineEndpoint to your host's IP (for example http://192.168.1.239:3300) — appmint_mobile does this directly.
Cleartext HTTP on Android
Android blocks cleartext HTTP by default in release builds. For local dev you'll hit a CLEARTEXT_NOT_PERMITTED error. Add a debug-only network security config in android/app/src/main/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
Reference it from AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
Production traffic to appengine.appmint.io is HTTPS, so this only affects debug builds.
With the environment file in place, the next page wires up the canonical AppmintClient that every screen will call.