Checkgate: Feature Flags That Don't Cost You a Network Round-Trip

Checkgate: Feature Flags That Don't Cost You a Network Round-Trip

An open-source, self-hosted feature flag engine that evaluates flags in-process (sub-microsecond) and syncs every SDK over SSE in under 50ms. Built in Rust, with native SDKs for Node, Web, React Native, and Flutter. Every feature-flag SaaS I've used shares the same architecture: your code calls isEnabled(), that call goes over the network to their servers, and you wait 5–50ms for an answer. It's fast enough that nobody complains in a demo, and slow enough that it shows up the moment you put a flag check in a hot path, an edge function, or a mobile app on a bad connection. So I built Checkgate — a self-hosted, open-source feature flag engine that flips the model: flags are evaluated in-process, in local memory, with no network call on the hot path. The server's only job is to push changes to every connected SDK the moment they happen. The core idea Most flag services treat evaluation as a remote procedure call. Checkgate treats it as a local cache lookup that happens to stay in sync in real time: Your app boots and the SDK opens one persistent SSE connection to the Checkgate server. The server bootstraps it with the full flag set for that environment. From then on, every isEnabled() is a pure, in-memory function call — no I/O, no allocations on the hot path. When someone flips a flag in the dashboard, the server pushes the delta over SSE, and every connected SDK instance is updated within ~50ms. ┌──────────────────────┐ SSE push, <50ms ┌─────────┐ │ Checkgate Server │ ───────────────────────────────▶ │ Node SDK │ │ (Rust + Postgres + │ ───────────────────────────────▶ │ Web SDK │ │ Redis pub/sub) │ ───────────────────────────────▶ │ RN SDK │ └──────────────────────┘ └─────────┘ isEnabled() = ~100ns, local Enter fullscreen mode Exit fullscreen mode The evaluation core (checkgate-core) is a Rust library compiled to different targets depending on the platform: Platform Binding Artifact Node.js NAPI-RS native .node addon Browser wasm-bindgen .wasm + JS glue React Native JSI (C FFI) .so / .dylib Flutter dart:ffi .so / .dylib Same Rust logic everywhere. No re-implemented rule engine per platform, no drift between what your Node backend decides and what your mobile app decides for the same user. It's not just a boolean toggle store A flag in Checkgate can be a boolean, string, integer, or JSON value, and evaluation runs through a well-defined pipeline: isEnabled(flag_key, user_key, attributes) │ ├── Prerequisites unsatisfied → false (checked first, fails closed) ├── Flag not found → false ├── flag.is_enabled == false → false │ ├── Targeting rules (first match wins) │ └── rule.attribute ∈ attributes AND operator matches → true │ └── Rollout percentage ├── 0% → false ├── 100% → true └── MurmurHash3(flag_key + ":" + user_key) % 100 < pct → true/false Enter fullscreen mode Exit fullscreen mode A few things that fall out of that: Targeting rules (equals, contains, starts_with, greater_than, etc.) always win over rollout percentage — so you can ship a feature at 5% globally while your whole team sees it via an email ends_with @yourcompany.com rule. Rollout percentage uses deterministic MurmurHash3 bucketing: the same user always lands in the same bucket, so bumping 10% → 20% only adds new users, it never reshuffles the cohort you already have. Segments let you define an audience ("Internal Employees", "Beta Users") once and reference it from any flag by key — they're expanded server-side, so SDKs never need to know segments exist. Prerequisite flags let one flag depend on another being enabled (or resolved to a specific value), evaluated recursively with a depth limit that fails closed on cycles. Weighted variants turn a flag into an A/B/n split — a 60/30/10 traffic split across control / treatment-a / treatment-b — using the same sticky hashing approach, salted independently so variant assignment doesn't correlate with the rollout gate. All of that lives entirely in the flag payload the SDK already has locally. No extra round-trip for a targeting decision, ever. Governance, because "who changed this flag in prod" is a real question Once more than one person can touch a flag, you need more than a toggle: RBAC — admin / editor / viewer, per project, independent of workspace-level admin. Change-request approvals — require a second reviewer before a change takes effect in sensitive environments; self-approval is blocked. Audit log of every change, plus scheduled changes and a cross-environment diff with one-click promote. Personal access tokens, scoped and revocable, as an alternative to handing out admin-equivalent SDK keys to CI/CD. Slack & Microsoft Teams alerts — flag and change-request activity land in a channel with native formatting (Block Kit for Slack, MessageCard for Teams), subscribable per event type (flag.updated, change_request.rejected, etc.). The ecosystem The server and SDKs are the core, but the parts that made Checkgate feel finished for real infrastructure are the boring-but-essential pieces: @checkgate/ssr — server-render initial flag state for Next.js/Remix/SvelteKit and hydrate with zero flag flicker. @checkgate/edge — a zero-dependency, runtime-agnostic evaluator for Cloudflare Workers / Fly.io that pulls a flag snapshot, caches it with TTL + stale-while-revalidate, and fails open on origin outages — delegating actual evaluation to the same WASM core every other SDK uses. @checkgate/cli — checkgate typegen generates type-safe flag accessors for TypeScript, Dart, and Rust, so a typo'd flag key is a compile error instead of a silent false. Terraform / OpenTofu provider and a Kubernetes operator (FeatureFlag CRD) for managing flags as code, with drift correction and require_approval awareness. Try it docker run -d -p 3000:3000 ghcr.io/thinkgrid-labs/checkgate:latest Enter fullscreen mode Exit fullscreen mode That single command pulls the published, multi-arch (amd64/arm64) all-in-one image — PostgreSQL, Redis, the Checkgate server, and the dashboard bundled together, no build step, no clone. Finish the setup wizard at localhost:3000/setup, grab the auto-generated SDK key, and: import { CheckgateClient } from '@checkgate/node' const client = new CheckgateClient({ serverUrl: 'http://localhost:3000', sdkKey: 'sk_live_your_key_here', }) await client.connect() const enabled = client.isEnabled('new-checkout-flow', 'user-123', { email: 'alice@example.com', plan: 'pro', }) Enter fullscreen mode Exit fullscreen mode The client connects once, downloads the flag set, and every subsequent isEnabled() is local. That's the whole pitch. Why self-host this instead of paying per-seat If you've priced out LaunchDarkly or Statsig at scale, the pitch for a self-hosted alternative is usually cost. The pitch for Checkgate specifically is: Evaluation latency goes from "network call" to "~100ns," which matters if you're flag-checking inside a request hot path, an edge worker, or a mobile app. Your user attributes never leave your infra — targeting attributes are evaluated locally and are never sent to the server, which matters if you have data-residency constraints. No vendor lock-in — it's Apache 2.0, single Rust binary + Postgres + Redis, and you can read every line of the evaluation logic that decides what your users see. It's still early — the roadmap includes a warehouse-export path and the A/B testing significance calculations are in beta — but the core (evaluation, SSE propagation, targeting, rollouts, governance) is solid enough to run in production today. Repo: https://github.com/ThinkGrid-Labs/checkgate Docs: https://thinkgrid-labs.github.io/checkgate If you try it, I'd genuinely like to hear where it breaks — issues and PRs are welcome.

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.