Architecture
Checkgate has three main layers: the control plane (server), the evaluation core (Rust library), and the SDK clients.
Overview
┌──────────────────────────────────────────────────────────────────┐
│ Checkgate Server │
│ │
│ Projects → Environments → Flags/Keys/Impressions │
│ │
│ ┌──────────────────────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ REST API │ │ In-Memory│ │ SSE Stream │ │
│ │ /api/projects/{id}/ │─▶│ FlagStore│─▶│ /stream │ │
│ │ environments/{id}/ │ └──────────┘ └───────────────┘ │
│ │ flags │ ▲ (per-env, scoped │
│ └──────────────────────────┘ │ by SDK key) │
│ │ │ Redis subscriber │
│ ▼ │ applies updates │
│ ┌──────────────┐ ┌──────────┐ │ │
│ │ PostgreSQL │─▶│ Redis │─────┘ │
│ │ (durable) │ │ (pub/sub)│ │
│ └──────────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────────┘
SSE (push on change, per-environment)
│
┌──────────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐
│Node.js │ │ Browser │ │ Flutter │ │ React Native │
│ SDK │ │ WASM │ │ FFI │ │ C FFI/JSI │
│(NAPI) │ │ SDK │ │ SDK │ │ SDK │
└────────┘ └──────────┘ └─────────┘ └──────────────┘
isEnabled() in-process, sub-microsecondData Model
The server organizes data in a strict four-level hierarchy:
Workspace (singleton — one per installation)
└── Projects
├── project_members (project_id, user_id, role)
└── Environments
├── sdk_keys (environment_id FK)
├── flags (environment_id FK)
└── impressions (environment_id FK)An SDK key is tied to one environment. When a client authenticates, the server uses the key to resolve both the project and the environment — no project_id or environment_id needs to be passed separately in the SDK.
Control Plane (Server)
The server is a single Rust binary built with Axum. It is responsible for:
- Storing projects, environments, flags, and users durably in PostgreSQL
- Maintaining an in-memory
FlagStorefor fast SSE bootstrap - Exposing a REST API for CRUD operations (project- and environment-scoped)
- Broadcasting flag changes to connected SDKs via SSE (
/stream), scoped per environment - Publishing change events to Redis pub/sub for multi-instance deployments
- Tracking flag evaluation events (impressions) from SDK clients
Write Path
When a flag is created or updated:
- The REST API handler writes the flag to PostgreSQL
- It publishes an
UPSERTevent (withenv_id) to thecheckgate_updatesRedis channel - The server's own Redis subscriber receives the event and updates the local in-memory
FlagStore - Connected SSE clients whose SDK key matches the flag's
environment_idreceive the event
The in-memory store is updated via the Redis subscriber loop, not directly by the write handler. This ensures the same code path runs for both local writes and cross-instance propagation.
Multi-Instance Deployments
When running multiple Checkgate server instances behind a load balancer:
- Each instance subscribes to Redis pub/sub on startup
- A write to any instance propagates to all others via Redis
- All instances stay in sync without direct inter-node communication
API Routes
Routes are organized by scope:
# Projects (workspace admin only for mutations)
GET /api/projects
POST /api/projects
PATCH /api/projects/{project_id}
DELETE /api/projects/{project_id}
# Project members
GET /api/projects/{project_id}/members
POST /api/projects/{project_id}/members
PATCH /api/projects/{project_id}/members/{user_id}
DELETE /api/projects/{project_id}/members/{user_id}
# Environments (project-scoped)
GET /api/projects/{project_id}/environments
POST /api/projects/{project_id}/environments
PATCH /api/projects/{project_id}/environments/{env_id}
DELETE /api/projects/{project_id}/environments/{env_id}
# SDK Keys (project-scoped, environment-targeted)
GET /api/projects/{project_id}/keys
POST /api/projects/{project_id}/keys body: { name, environment_id }
DELETE /api/projects/{project_id}/keys/{id}
# Flags (environment-scoped)
GET/POST /api/environments/{env_id}/flags
GET/PATCH/DELETE /api/environments/{env_id}/flags/{key}
POST /api/environments/{env_id}/flags/{key}/promote
# Impressions
GET /api/environments/{env_id}/impressions ?flag_key= &user_id= &value= &since_id=
GET /api/environments/{env_id}/impressions/stats
POST /api/environments/{env_id}/impressionsEvaluation Core (checkgate-core)
The core is a Rust library (core/) compiled into each SDK:
FlagStore— thread-safeDashMapholding the in-memory flag cacheevaluate(flag, user_context)— pure function, no I/O, no allocations on the hot path
Evaluation Logic
isEnabled(flag_key, user_key, attributes)
│
├── 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 < percentage → true/falseTargeting rules bypass the rollout percentage. A user matching a targeting rule always gets true, even if the rollout is 0%.
Hashing
Rollout uses MurmurHash3 (x86/32-bit) for:
- Speed — non-cryptographic, very fast
- Stability — same user + flag always maps to the same bucket
- Distribution — uniform bucketing across the 0–99 range
SSE Stream
The SSE stream (GET /stream) connects SDK clients to the server for real-time flag updates. It is scoped per environment.
Environment Scoping via SDK Key
Each SDK key is stored with an environment_id in the database. When a client connects:
- The server resolves the SDK key to an
environment_id - It bootstraps the client with only the flags belonging to that environment (queried from PostgreSQL)
- All subsequent live events are filtered to that
environment_id
This means a single Checkgate installation can serve multiple projects with multiple environments, with each SDK client receiving only the flags relevant to its environment.
Connection Lifecycle
- SDK connects to
GET /streamwithAuthorization: Bearer sk_live_... - Server looks up the key → resolves
environment_id - Server sends
connectedevent — SDK clears its local store - Server bootstraps the SDK by replaying all current flags for that environment
- Server sends
keep-alivepings every 15 seconds - On flag change, only clients in the matching environment receive the update
- On reconnect, the SDK clears its local store and re-bootstraps from scratch
Events
| Event | Payload | Description |
|---|---|---|
connected | "true" | Connection established; SDK resets its store |
update | JSON string | A flag was upserted or deleted in this environment |
| (keep-alive) | keep-alive-text | Heartbeat every 15 seconds |
Authentication
Two separate authentication mechanisms:
| Credential | Used by | How |
|---|---|---|
| Email + password | Dashboard users | Issues an HttpOnly AES-256-GCM encrypted session cookie |
SDK key (sk_live_...) | SDK clients, CI/CD | Authorization: Bearer <key> or ?sdk_key= query param |
SDK keys are stored in PostgreSQL, managed per-project from the Project Settings → SDK Keys tab. Each key is bound to one environment — it implicitly identifies both the project and the environment for all API calls and the SSE stream.
Role Enforcement
| Route category | Required role |
|---|---|
| Read flags, impressions | Any authenticated user with project access |
| Write flags | editor or admin (project member) |
| Manage environments, SDK keys, project members | admin (project member) |
| Manage workspace users, create/delete projects | Workspace admin |
Workspace admins bypass all per-project membership checks.
Impression Tracking
SDKs report evaluation events asynchronously:
POST /api/environments/{env_id}/impressions
Authorization: Bearer <sdk_key>
[{"flag_key": "checkout_v2", "user_id": "u123", "value": "true", "context": {...}}]Impressions are stored in the impressions table (indexed by environment_id, flag_key, evaluated_at). The dashboard surfaces them on two tabs:
- Analytics — per-flag aggregates: total evaluations, true/false split, unique users
- Stream — live polling log (refreshes every 3s), filterable by
flag_key,user_id, andvalue; context JSON expandable inline
The GET /api/environments/{env_id}/impressions endpoint supports since_id for efficient incremental polling — the stream tab uses this to fetch only new rows since the last poll.
SDK Clients
Each SDK maintains its own local copy of the flag store:
| SDK | Binding | Binary |
|---|---|---|
| Node.js | NAPI-RS | .node native addon |
| Browser | wasm-bindgen | .wasm + JS glue |
| React Native | C FFI via JSI | .so / .dylib |
| Flutter | dart:ffi | .so / .dylib |
The SDK key passed at initialization determines which project and environment the client receives. No project ID or environment ID needs to be specified separately in the SDK — the key encodes that association server-side.
