ConfigWire is a single-binary remote configuration and feature flagging service built for teams that demand
sub-millisecond evaluation, zero vendor lock-in, and instant real-time synchronization.
Unlike complex multi-tier services that depend on distributed database clusters (PostgreSQL, MySQL, Redis)
or proprietary cloud backends (Firebase, LaunchDarkly), ConfigWire packages everything into a self-contained
Go binary with embedded SQLite in Write-Ahead Logging (WAL) mode.
Performance Benchmark (k6)
Under continuous load testing with 100 rps fetch + 50 rps exposure event ingest, ConfigWire achieves a
p95 latency of 0.78ms with 0.00% failed requests.
Key Architectural Pillars
Single Binary Simplicity: Runs directly on any server or minimal Docker container. No
external database orchestration required.
Immutable Release Snapshots: Every configuration change produces a versioned snapshot
with an SHA-256 ETag. Releases cannot be altered after publication.
Instant Fan-Out: Publishes broadcast immediately to connected clients over HTTP
Server-Sent Events (SSE).
Edge-Ready Caching: Clients store evaluated configurations locally and revalidate via
HTTP 304 Not Modified with zero CPU recomputation.
2. Installation & Quickstart
ConfigWire distributes ready-to-run container images via GitHub Container Registry (GHCR) as well as
standalone Go binaries.
Option A: Docker Run (Fastest)
Execute this single command to pull and launch ConfigWire with a persistent data volume:
Running from source instead of Docker: serve must run from
configwire/ so ./pb_public resolves — from anywhere else /
answers 404 while /healthz stays 200.
Ports:8090 default serve; 8108 e2e reserved
(scripts/e2e.sh, do not reuse); 8109 example only;
8106/8120 historical scratch ports per ROTATION.md.
Prereqs: Docker (above) or Go (floor per configwire/go.mod); Dart SDK
>=3.12.0 for the client; Node >=20 for js_tests only.
3. First-Run Super Account Setup
ConfigWire has its own built-in initial setup flow directly on the main homepage. You do
not need to visit PocketBase's underlying /_/ installer interface.
Direct Setup via ConfigWire Homepage
1. Open your browser and navigate to http://localhost:8090/.
2. The web client queries GET /api/v1/admin/setup/status. Because no administrator exists
yet, ConfigWire displays the native First-Run Superuser Setup Modal.
3. Fill in your Administrator Email and Password (8-72 characters).
4. Submitting dispatches POST /api/v1/admin/setup, securely provisioning the root superuser
and immediately logging you into the dark dashboard.
Environment Preseed (Optional)
To skip the browser entirely, set both variables before first boot — the server creates the
superuser itself (empty keeps the setup modal):
[email protected] CONFIGWIRE_ADMIN_PASSWORD='<8-72-chars>' docker compose up -d
CLI Fallback Setup (Optional)
If you prefer provisioning the superuser via shell command without opening the browser:
Multiple projects can each have an environment named dev. On client fetch and streaming routes,
the SDK Key deterministically resolves to the exact environment it was minted for, avoiding misrouting.
For admin endpoints, pass the optional ?project=<projectId> query parameter if two
projects share identical slug names.
string - Textual values, URLs, headline variants, or localization keys.
number - Integer or floating point numbers (e.g. rate limits, retry counts, pricing).
json - Structured objects and lists for dynamic UI configurations.
Flag Key Validation Rule
Flag keys must match ^[A-Za-z_][A-Za-z0-9_.-]*$ and be between 1 and 128 characters long.
Flag volume is unlimited: no per-project flag cap.
6. Releases & Snapshot Versioning
Whenever you publish changes in an environment, ConfigWire produces an immutable Release
row.
Concurrency Protection (baseVersion)
Publishing requires sending the environment's current version as baseVersion. If another
teammate published a release in the meantime, the server rejects the request with 409 Conflict:
Each release is hashed: etag = hex(sha256(version + ":" + snapshot))[:16]. Clients send this
back as If-None-Match, answering 304 Not Modified with zero overhead when
unchanged.
7. Rules & Multi-Condition Targeting
Rules allow dynamically overriding default values based on caller attributes.
v0.2.0-rc.11 (released 2026-11-04) is bucket-only (since v0.2.0-rc.1): ?b=0-9999 is the sole
identity: bucketing rides
fnv64a(?b= + seed) % 10000 (valid wins; absent/empty never rejected — fetch
mints, ingest stores ""; malformed non-empty ?b= → 400).
Server-derived anonID derivation no longer exists (no daily salt rotation, no rebucket) and SDKs no
longer emit ?userHash=. Legacy ?userHash=, ?uid=,
?country= query inputs plus body userHash and event-body
userId (any case) are accepted-but-ignored, never stored/logged.
Each rule condition supports:
Sole bucketing key: stable ?b= bucket
(Bucket(?b=+seed) = fnv64a % 10000). v0.2.0-rc.1 (released 2026-10-25; latest v0.2.0-rc.11 — 2026-11-04) removed the
server-derived anonID derivation entirely — no fallback, no daily salt rotation, no rebucket.
Up to 10 condition objects can be combined and AND-ed in a single rule.
8. A/B Testing & Experiments
ConfigWire features built-in deterministic variant bucketing (64-bit FNV-1a):
Variants are assigned weights summing to exactly 10,000 basis points (100.00%).
Bucket-only in v0.2.0-rc.11 (released 2026-11-04; since v0.2.0-rc.1):Bucket = fnv64a(?b= + seed) % 10000 is the sole identity. Server-derived anonID
derivation no longer exists — no fallback, no daily salt rotation, no rebucket.
The same ?b= keeps the same arm.
The server returns the assigned arm in the variants payload (e.g.
{"checkout_btn": "treatment_b"}).
9. Events Ingest & 90-Day Analytics (tunable 1..1825 via PUT /api/v1/admin/retention)
Track user exposures and fetches via POST /api/v1/events (canonical; POST /api/v1/env/{env}/events backward-compatible).
Privacy Guard: Zero Raw PII
v0.2.0-rc.11 (released 2026-11-04) is bucket-only (since v0.2.0-rc.1): send ?b= to join
exposures to the fetch arm (absent/empty never rejected — ingest stores ""). Raw
ip plus PII/device keys (email, phone,
deviceId, …) stay strict 400. Event-body userId (any case)
and body userHash are accepted-but-ignored, never stored/logged — send no raw identity.
Retention Schedule (tunable 1..1825 via PUT /api/v1/admin/retention)
30d raw: Raw events are retained in full detail (default 30d).
90d rollups: Raw rows are rolled up into daily aggregate buckets
(event_daily keyed (day, env, flag, variant, version)) and purged, keeping
storage footprint tiny (default 90d).
Stats Endpoint:GET /api/v1/admin/env/{env}/stats?since=7d[&flag=<key>] merges raw events and daily
rollups into continuous time-series statistics. Without ?flag= counts are env-wide with
flagFound: true and echo.flag: ""; with ?flag= counts filter to
that flag and echo.flag echoes the key with flagFound: true when known. Unknown
flag returns zeros with flagFound: false and 200. Merged windows answer
approximate: true; events-only windows are exact.
10. Realtime SSE Streaming Hub
Clients establish an unbuffered HTTP stream via GET /api/v1/stream (canonical; GET /api/v1/env/{env}/stream backward-compatible).
Best-effort near-instant (while connected): When a release is published, the server emits a
config_update frame with the new version and ETag. Push is best-effort: a slow/dead
subscriber's queue overflows into a drop, never a block, and with N processes a subscriber connected
to another process misses the push and converges via the poll fallback. Auth order is 401 (key) then
404 (unknown env) then 401 (key not scoped to this env).
Keepalive Heartbeats: The server sends a : ping comment every 20 seconds,
and closes the stream at a 10-minute cap (client reconnects with backoff).
Connection Limits: Up to 5 concurrent streams per SDK key (over cap → 429).
Fallback Polling: Paired with 15-minute background polling so devices never miss
updates during network dropouts.
11. Rollbacks & Environment Promotion
Instant Rollback
To revert a faulty rollout, post to the rollback endpoint. ConfigWire clones the previous snapshot into a
brand new release version:
curl -X POST http://localhost:8090/api/v1/admin/env/dev/releases/1/rollback \
-H "Authorization: $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note": "reverting regression in v2"}'
Promote from Dev to Prod
Promote tested configurations between environments within the same project with zero re-entry:
Full shapes live in configwire/docs/CONTRACT.md (canonical) — the table above is a summary.
Identity note: v0.2.0-rc.11 (released 2026-11-04) is bucket-only (since v0.2.0-rc.1):
?b=0-9999 is the sole identity (valid wins; absent/empty never rejected —
fetch mints, ingest stores ""; malformed ?b= → 400).
Server-derived anonID derivation no longer exists. Legacy ?userHash=,
?uid=, ?country= plus body userHash and event-body
userId (any case) are accepted-but-ignored, never 400, never stored/logged.
Send targeting context via
attrs.
14. Dart & Flutter SDK Guide
The official configwire package is pure Dart with zero Flutter dependencies, making it
universally compatible across Flutter iOS, Android, macOS, Windows, Linux, Web, Dart VM, and backend
servers.
Installation
dart pub add configwire
# Or for Flutter apps (prefs cache + device targeting):
flutter pub add configwire_flutter
Pluggable Disk Cache Seam (CacheStore)
By default, ConfigWire uses MemoryCacheStore (session only). For disk persistence without
Flutter dependencies, implement the two-method CacheStore interface:
env: in these examples is deprecated but backward-compatible: the key-only paths are canonical per the wire
contract; the env paths still work.
Flutter Companion (configwire_flutter)
Flutter apps should use the configwire_flutter companion instead of wiring the pure-Dart client
by hand.
One call supplies the two Flutter-specific pieces the pure-Dart client cannot provide on its own:
a SharedPreferencesAsync-backed CacheStore and device-derived
Targeting
(platform, build-stripped app version, locale/country, plus a stable ?b= bucket join —
v0.2.0-rc.11, released 2026-11-04 (bucket-only since v0.2.0-rc.1), with no server-derived anonID, no daily salt rotation,
and no rebucket). Install-ID generation/emission is removed (no Targeting.userId, no
install-ID helpers or prefs keys, no ?userHash=); ?b= is the sole identity.
The server accepts-but-ignores any
legacy ?userHash=/?uid=/?country=, body
userHash, or event-body userId (any case) — never 400, never
stored/logged — stamping X-CW-Deprecated-* when trimmed non-empty).
The returned ConfigWire is a plain instance, so every Dart API above behaves exactly as
documented.
Older ConfigWireFlutter.createConfigWire(...) calls still work via a deprecated shim.
A runnable reference lives in the package example/ (flag-gated UI from
welcome_message and new_checkout flags, realtime rebuild via
onUpdate,
trackExposure at the flag-driven branches, and a debug panel with fetch status, version, etag,
and targeting):
github.com/configwire/flutter/tree/main/example.
We use Umami for visit statistics (no tracking cookies).
Enabled only if you accept. See our privacy policy.