ConfigWire Documentation & Guides

1. Overview & Architecture

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:

Terminal (bash)
docker run -d \
  --name configwire \
  --restart unless-stopped \
  -p 8090:8090 \
  -v cw-data:/app/pb_data \
  ghcr.io/configwire/configwire:latest

Option B: Docker Compose

Create a compose.yml file in your project directory:

compose.yml
services:
  configwire:
    image: ghcr.io/configwire/configwire:latest
    container_name: configwire
    ports:
      - "8090:8090"
    volumes:
      - cw-data:/app/pb_data
    environment:
      - CONFIGWIRE_ADMIN_EMAIL=${CONFIGWIRE_ADMIN_EMAIL:-}
      - CONFIGWIRE_ADMIN_PASSWORD=${CONFIGWIRE_ADMIN_PASSWORD:-}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8090/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3

volumes:
  cw-data:

Then run:

Terminal (bash)
docker compose up -d

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:

docker compose exec configwire ./configwire superuser upsert [email protected] password123 --dir /app/pb_data

4. Projects & Environments

ConfigWire organizes configurations hierarchically:

Project (e.g., "Mobile App", "Checkout Service")
  ├── Environment "dev"    (isolated flags, keys & releases)
  ├── Environment "staging"
  └── Environment "prod"

Environment Slug Disambiguation

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.

5. Flags & Supported Data Types

ConfigWire supports four first-class flag types:

  • bool - Boolean flags (true / false) for instant feature toggling.
  • 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:

{
  "message": "Stale baseVersion: a newer release exists.",
  "status": 409,
  "currentVersion": 3
}

ETag Computation

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.
  • Request context: platform (e.g. ios, android, web), appVersion (strict semver MAJOR.MINOR.PATCH; unparseable falls through), locale, country (server geo).
  • Custom attributes: Any nested field passed inside the JSON attrs payload (e.g., tier, betaTester, companyId). PII/device keys (email, phone, ip, deviceId, …) are rejected with 400; attrs over 8192 bytes returns 414.
  • Operators: ==, !=, <, <=, >, >=, contains, regex (plus <=/between for percentile rules).

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:

curl -X POST http://localhost:8090/api/v1/admin/env/prod/promote \
  -H "Authorization: $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"srcEnv": "dev", "srcVersion": 3, "destBaseVersion": 1, "note": "promoted release 3"}'

12. Security, Rate Limits & IP Allowlists

ConfigWire includes defense-in-depth protection:

  • Opaque Bcrypt Keys: Keys are prefixed with cw- followed by 24 cryptographic base62 characters. The server stores only bcrypt verifiers.
  • Rate Limiting: Global per-IP 200 rps effective (min 200, burst 400 ceiling) + per-key limits (100 rps fetch, 50 rps ingest, configurable up to 10,000 rps). Exceeded requests receive 429 with Retry-After: 1.
  • Admin IP CIDR Allowlist: Restrict admin routes to corporate IPs/VPNs. Unlisted IPs receive 403 Forbidden.
  • Last Admin Guard: Code hooks prevent deleting the last remaining superuser on all paths.

13. CLI & REST Wire Contract

Method Endpoint Auth Description
GET /healthz Public Service healthcheck (returns 200 OK)
GET /api/v1/admin/setup/status Public Checks if first-run superuser exists
POST /api/v1/admin/setup Public (once) Creates first superuser account
GET /api/v1/config (canonical; /api/v1/env/{env}/config backward-compatible) SDK Key Evaluates and returns flag values
POST /api/v1/events (canonical; /api/v1/env/{env}/events backward-compatible) SDK Key Ingests fetch & exposure analytics
GET /api/v1/stream (canonical; /api/v1/env/{env}/stream backward-compatible) SDK Key Server-Sent Events (SSE) realtime push
POST /api/v1/admin/env/{env}/publish Admin Token Publishes release snapshot
GET /api/v1/admin/env/{env}/stats Admin Token Queries 7d-90d analytics time-series
POST /api/v1/admin/keys Admin Token Mints SDK key (201 shows key once)
GET/PUT /api/v1/admin/limits Admin Token Reads/tunes global + per-key rate limits
GET/PUT /api/v1/admin/retention Admin Token Reads/tunes raw 30d + rollup 90d windows
POST /api/v1/admin/maintenance/purge Admin Token Runs 48h scrub + retention purge (?dry=1 dry-run)
POST /api/v1/admin/env/{env}/releases/{v}/rollback Admin Token Copies snapshot to new version (fresh etag)
POST /api/v1/admin/env/{dest}/promote Admin Token Clones src release to dest env (same project)
GET /api/v1/admin/auth/mode Public Returns standard|sso (+ cloudUrl when set)
POST /api/v1/admin/sso/redeem Public Redeems 60s ticket for cw_admin cookie
GET/POST /api/backups Admin Token Lists/creates SQLite backup zips (see BACKUPS.md)

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:

file_store.dart
import 'dart:convert';
import 'dart:io';
import 'package:configwire/configwire.dart';

class JsonFileStore implements CacheStore {
  JsonFileStore(this.file);
  final File file;

  @override
  Future<CacheData?> load() async {
    try {
      final decoded = jsonDecode(await file.readAsString());
      if (decoded is! Map) return null;
      return CacheData.fromJson(Map<String, Object?>.from(decoded));
    } catch (_) {
      return null;
    }
  }

  @override
  Future<void> save(CacheData data) async {
    await file.parent.create(recursive: true);
    await file.writeAsString(jsonEncode(data.toJson()));
  }
}

Complete Integration Example

main.dart
final cw = ConfigWire(
  apiKey: 'cw-Ab12CdEfGhIjKlMnOpQrStUv',
  env: 'dev',
  baseUrl: 'http://127.0.0.1:8090',
  defaults: {
    'launch_flag': false,
    'hero_title': 'Welcome!',
    'retries': 3,
  },
  store: JsonFileStore(File('.cache/configwire.json')),
);

// Initialize: loads disk cache, then triggers non-throttled fetch
await cw.ensureInitialized();

// Read typed values synchronously
final isFeatureOn = cw.getBool('launch_flag');
final heroText = cw.getString('hero_title');

// Subscribe to realtime updates
await cw.connectRealtime();
cw.onUpdate.listen((values) {
  print('Received remote update: $values');
});

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.

flutter_main.dart
import 'package:configwire_flutter/configwire_flutter.dart';

final cw = ConfigWire(
  apiKey: 'cw-YourKey123',
  env: 'dev',
  baseUrl: 'http://127.0.0.1:8090',
  defaults: {'launch_flag': false},
);
await cw.configureFlutter(customAttrs: {'plan': 'pro'});
await cw.ensureInitialized();

final isEnabled = cw.getBool('launch_flag');

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.