cloudflare_worker_kv 0.1.0
cloudflare_worker_kv: ^0.1.0 copied to clipboard
Remote config for Flutter backed by Cloudflare Workers KV: in-app defaults, fetch and activate, typed getters and an offline cache.
cloudflare_worker_kv #
Remote config for Flutter, backed by Cloudflare Workers KV.
- In-app defaults,
fetch/activate/fetchAndActivate - Typed getters:
getString,getBool,getInt,getDouble,getValue,getAll RemoteConfigSettingswithfetchTimeoutandminimumFetchInterval- Offline cache: the last activated config is restored on the next launch
- Bandwidth-friendly:
ETag/304 Not Modified - Works on Android, iOS, macOS, Windows, Linux and web
Demo #

The example app: welcome_message is changed in
Workers KV, then Fetch & activate picks up the new value.
How it works #
The package talks to a small read-only Cloudflare Worker, so your app never
holds a Cloudflare API token. The Worker, and how to deploy it against your KV
namespace, lives in the
worker/ module
of this repository. Any backend that implements the HTTP contract
works too.
Getting started #
You need the URL of a deployed Worker, e.g.
https://my-config.<subdomain>.workers.dev.
dependencies:
cloudflare_worker_kv: ^0.1.0
import 'package:cloudflare_worker_kv/cloudflare_worker_kv.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final remoteConfig = await CloudflareRemoteConfig.initialize(
endpoint: Uri.parse('https://my-config.<subdomain>.workers.dev'),
clientKey: 'optional-client-key', // only if the Worker sets CLIENT_KEY
);
await remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(seconds: 10),
minimumFetchInterval: const Duration(hours: 1),
));
await remoteConfig.setDefaults(const {
'welcome_message': 'Hello',
'max_items': 10,
'new_checkout_enabled': false,
});
try {
await remoteConfig.fetchAndActivate();
} on RemoteConfigException catch (e) {
// Offline, timeout, ... cached or default values are still available.
debugPrint('Remote config fetch failed: $e');
}
runApp(MyApp());
}
// Anywhere in the app:
final rc = CloudflareRemoteConfig.instance;
final maxItems = rc.getInt('max_items');
final newCheckout = rc.getBool('new_checkout_enabled');
Values are delivered as strings and converted by the typed getters. A parameter stored as a JSON object or array arrives as JSON text:
final units = jsonDecode(rc.getString('ad_units')) as Map<String, dynamic>;
See example/ for a complete app.
Migrating from Firebase Remote Config #
| firebase_remote_config | cloudflare_worker_kv |
|---|---|
FirebaseRemoteConfig.instance |
CloudflareRemoteConfig.instance (after initialize) |
setConfigSettings(RemoteConfigSettings) |
same |
setDefaults(Map) |
same |
ensureInitialized() |
same (done by initialize) |
fetch() / activate() / fetchAndActivate() |
same |
getString/Bool/Int/Double/Value/getAll |
same |
lastFetchTime, lastFetchStatus, settings |
same |
RemoteConfigValue, ValueSource |
same |
FirebaseException |
RemoteConfigException (code, statusCode) |
onConfigUpdated |
not yet (see roadmap) |
| Conditions, A/B testing, personalization | not supported |
Behaviour #
- Value resolution: activated remote value → default → static
(
'',0,0.0,false).getValue(key).sourcetells you which. - Conversions:
asBool()istruefor1, true, t, yes, y, on(case-insensitive).asInt()/asDouble()return0/0.0when the value cannot be parsed. Conversions never throw. fetch()never changes what the getters return; callactivate(). WithinminimumFetchIntervalof the last successful fetch it completes without a network request. Concurrent calls share one request.activate()returnstrueonly when a fetched config differs from the active one.- Errors:
fetch()throwsRemoteConfigExceptionwith acodeoftimeout,network-error,unauthorized,throttled,server-errororinvalid-response. A failed fetch never touches the active config. HTTP 429 setslastFetchStatustothrottleand blocks fetches untilthrottleEndTime(fromRetry-After, default 1 minute). - Freshness: a change made in KV reaches the Worker after about 1–2
minutes. The app picks it up on its next fetch, which
minimumFetchIntervalmay delay. - Persistence: active and pending config,
ETag, settings and fetch status are stored withshared_preferences(SharedPreferencesAsync). Provide your ownConfigStorageto change that. Defaults are not persisted — set them on every launch. - Multiple configs: create extra instances with
CloudflareRemoteConfig(endpoint: ..., template: 'staging'); each endpoint/template pair has its own cache.
Security #
- The Worker only exposes read access to your config. Do not put secrets in remote config — anyone with the app can read it.
clientKeyis a shared value compiled into the app. It keeps casual traffic away from your Worker but is not a secret. Combine with Cloudflare rate limiting if you need abuse protection.
Platform setup #
- Android: release builds need
<uses-permission android:name="android.permission.INTERNET" />. - macOS: add
com.apple.security.network.clientto bothDebugProfile.entitlementsandRelease.entitlements. - Web: the Worker sends CORS headers, no extra setup needed.
HTTP contract #
GET {endpoint}/v1/config?template={template}
Request headers: X-Client-Key (optional), If-None-Match (optional)
200 {"version": "<opaque>", "entries": {"key": "value", ...}} + ETag header
304 when If-None-Match matches
401/403 bad client key · 429 rate limited (Retry-After) · 5xx errors
Running the tests #
flutter test
Roadmap #
onConfigUpdatedstream (polling +ETag)- Conditional values (platform, app version, percentage rollout)
- Authenticated admin endpoint for publishing config
