DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

Building a Real-Time, Event-Sourced Feature Flag System with Rust and WebAssembly

Building a Real-Time, Event-Sourced Feature Flag System with Rust and WebAssembly

Building a Real-Time, Event-Sourced Feature Flag System with Rust and WebAssembly

Disabling or enabling features at runtime is a common need in modern software. A robust feature flag system lets product teams experiment safely, roll out changes gradually, and reduce blast radius during incidents. In this tutorial, you’ll build a lightweight, real-time, event-sourced feature flag service composed of: a Rust-based event store, a WebAssembly (wasm) client surface, and a small Node.js server for orchestration. The approach emphasizes correctness, observability, and low latency, while staying approachable for practical use in real projects.

What you’ll learn

  • Principles of event sourcing for feature flags and how it improves auditability and rollbacks
  • Designing a compact event schema for feature flag lifecycle events
  • Implementing a Rust event store with append-only logs and snapshotting
  • Exposing a WASM module that can be embedded in frontend apps to evaluate feature flags with minimal latency
  • Real-time updates via server-sent events (SSE) and a simple poller fallback
  • Basic authorization, schema migrations, and observability considerations

Overview of the architecture

  • Event Store (Rust): An append-only log backed by a simple file-based database that supports snapshots. It stores events such as FlagCreated, FlagUpdated, FlagDeleted, and FlagEval.
  • WASM Client (Rust/WASM): A small library compiled to WebAssembly that provides a high-performance feature flag evaluator. It subscribes to events and applies the latest state to determine whether a flag is enabled for a user or context.
  • API Server (Node.js): A tiny HTTP server that serves the WASM module, provides endpoints for creating/updating flags, and streams updates to clients via Server-Sent Events.
  • Frontend integration: The WASM evaluator can be instantiated in the browser, receiving real-time updates and evaluating flags per user context.

Prerequisites

  • Knowledge of Rust basics (ownership, structs, enums)
  • Basic understanding of WebAssembly and how to use Rust to compile to wasm
  • Node.js installed for the API server
  • A small HTTP client or Postman for testing endpoints
  • Optional: Docker to run components in containers

Project layout

  • event-store/ (Rust crate for the event store)
  • wasm-evaluator/ (Rust crate compiled to wasm)
  • api-server/ (Node.js server to manage flags and SSE)

Part 1: Designing the event schema

We’ll keep the event schema compact and forward-compatible. Each event has a type, a flag_id, a version, a timestamp, and a payload.

  • FlagCreated: { flag_id, name, description, enabled_by_default, variants }
  • FlagUpdated: { flag_id, name?, description?, enabled_by_default?, variants? }
  • FlagDeleted: { flag_id }
  • FlagEval: { flag_id, context (e.g., user_id, ip, cohort), enabled (bool), reason }

Event ordering guarantees deterministic state reconstruction.

Part 2: Implementing the Rust event store

Goal: provide append-only writes, read current state, and emit deltas for subscribers.

Key components

  • Event enum with serde serialization
  • Append-only log stored as files per flag or a single log file
  • In-memory index to quickly compute current flag state
  • Snapshot mechanism to speed startup

Code sketch (Rust)

  • Cargo.toml dependencies: serde, serde_json, chrono, tokio, futures, tracing

Main structures

  • struct Event { id: Uuid, event_type: EventType, flag_id: String, version: u64, timestamp: DateTime, payload: serde_json::Value }
  • enum EventType { FlagCreated, FlagUpdated, FlagDeleted, FlagEval }

Core functions

  • append_event(flag_id, event_type, payload)
  • load_events(flag_id) -> Vec
  • build_snapshot(flag_id) -> FlagState
  • subscribe_updates() -> Stream

Example (high-level, simplified)

  • When FlagCreated arrives, create initial state: { flag_id, name, enabled_by_default, variants, is_deleted: false, version: 1 }
  • FlagUpdated merges fields; increment version
  • FlagDeleted marks is_deleted and sets version

Notes

  • Use a simple file-per-flag storage for clarity, or a single log with rotation.
  • Implement compaction: periodically create a snapshot file representing the current flag state, and trim older events.

Part 3: Building the WASM evaluator

Goal: deliver a fast, deterministic evaluation function that frontend apps can use locally.

Why WASM? Reduces JS overhead for frequent evaluations and enables offline or offline-ish experiences.

WASM design

  • The evaluator exposes a function evaluate(flag_state, context) -> bool
  • flag_state is a compact representation: flag_id, enabled_by_default, variants, current_value (derived)
  • context is a small map-like structure (user_id, cohort, environment)

Rust code outline (lib.rs)

  • #[wasm_bindgen] pub fn evaluate(flag_state_json: &str, context_json: &str) -> bool
  • Deserialize flag_state and context using serde_json
  • Apply rule: if there is a flag-specific Override or a default, return the evaluated boolean
  • Return a boolean that frontend can consume

Build steps

  • Use wasm-pack to compile to wasm
  • Ensure compatibility with popular bundlers (webpack, esbuild)
  • Export the evaluate function and a simple init for subscriptions

Frontend surface

  • A small wrapper around the wasm module that:

    • Maintains the latest flag_state from the server
    • Provides a function is_enabled(flag_id, context) that calls into wasm
    • Subscribes to SSE for real-time updates and triggers wasm state refresh

Part 4: The API server (Node.js)

Responsibilities

  • Create, update, delete flags
  • Append events to the Rust event store
  • Serve the WASM module to clients
  • Provide an SSE endpoint /flags/stream that streams FlagUpdated and FlagDeleted events

Security and validation

  • Simple API key check for demo
  • Validate payload schemas with a lightweight validator
  • Rate-limit to protect the endpoint

Example endpoints

  • POST /flags -> create a new flag Body: { flag_id, name, description, enabled_by_default, variants }
  • PATCH /flags/:flag_id -> update a flag Body: { name?, description?, enabled_by_default?, variants? }
  • DELETE /flags/:flag_id -> delete a flag
  • GET /wasm/flag-evaluator.wasm -> serve wasm module
  • GET /flags/stream -> SSE stream of events

Implementation notes

  • On server start, load the current state from the event store to seed the wasm evaluator
  • When an event is appended, push the event to all connected SSE clients
  • Provide a small helper to convert events into a delta that clients can apply incrementally

Part 5: Observability and testing

Observability

  • Structured logging with tracing for Rust components
  • Metrics: event write rate, lag between event time and processed time
  • Health endpoints for each component

Testing strategy

  • Unit tests for event application logic in Rust
  • Integration test simulating creating a flag, updating it, and streaming events
  • End-to-end test with API server and wasm evaluator

Concrete code examples

Note: The following are condensed snippets to illustrate key ideas. They are not a full, runnable project but provide concrete patterns you can implement.

Rust event (serde-based)

use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use serde_json::Value;

[derive(Serialize, Deserialize, Debug, Clone)]

pub enum EventType {
FlagCreated,
FlagUpdated,
FlagDeleted,
FlagEval,
}

[derive(Serialize, Deserialize, Debug, Clone)]

pub struct Event {
pub id: String, // UUID
pub event_type: EventType,
pub flag_id: String,
pub version: u64,
pub timestamp: DateTime,
pub payload: Value,
}

impl Event {
pub fn new(event_type: EventType, flag_id: &str, version: u64, payload: Value) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
event_type,
flag_id: flag_id.to_string(),
version,
timestamp: Utc::now(),
payload,
}
}
}

WASM evaluator (lib.rs, simplified)

use wasm_bindgen::prelude::*;
use serde_json::Value;

[wasm_bindgen]

pub fn evaluate(flag_state_json: &str, context_json: &str) -> bool {
// Basic deserialization
let state: Value = serde_json::from_str(flag_state_json).unwrap_or_else(|| Value::Null);
let context: Value = serde_json::from_str(context_json).unwrap_or_else(|
| Value::Null);

// Simple logic: if flag has "enabled" boolean in state, return it; otherwise default
if let Some(enabled) = state.get("enabled").and_then(|v| v.as_bool()) {
return enabled;
}
// Fallback to enabled_by_default
if let Some(def) = state.get("enabled_by_default").and_then(|v| v.as_bool()) {
return def;
}
false
}

api-server (Node.js, sketch)

const express = require('express');
const { Readable } = require('stream');
const app = express();
app.use(express.json());

let events = []; // In-memory for demo; in production, pull from Rust event store

app.post('/flags', (req, res) => {
const { flag_id, name, description, enabled_by_default, variants } = req.body;
const e = { event_type: 'FlagCreated', flag_id, version: 1, payload: { name, description, enabled_by_default, variants } };
events.push(e);
res.status(201).json({ ok: true, event: e });
});

app.get('/flags/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.flushHeaders();

const onEvent = (ev) => {
res.write(data: ${JSON.stringify(ev)}\n\n);
};

// In a real app, subscribe to Rust event store
// Here we simulate with a timer or immediate push
const id = setInterval(() => {
if (events.length) onEvent(events[events.length - 1]);
}, 1000);

req.on('close', () => clearInterval(id));
});

app.listen(3000, () => {
console.log('API server listening on port 3000');
});

How to run (high level)

  • Build the Rust event store:
    • cd event-store
    • cargo build release
  • Build the WASM evaluator:
    • cd wasm-evaluator
    • wasm-pack build target web
  • Run API server:
    • cd api-server
    • npm install
    • node index.js

Putting it together in practice

  • Start by implementing the event store with a simple append-only log. Write tests that ensure events are replayable to reconstruct state.
  • Build the wasm evaluator and expose a minimal evaluate function. Integrate it into a small frontend page that loads the wasm, fetches flag state, and evaluates a few sample contexts.
  • Create a lightweight API to manage flags and emit events. Ensure the SSE stream delivers incremental updates and that clients can rebuild state from the event log if needed.
  • Iterate on observability: add tracing spans around event writes and state recomputation; add a simple dashboard to show current flag states and last update times.

Next steps and refinements

  • Extend the event schema with per-flag evaluation rules (e.g., percentage rollouts, user cohorts).
  • Add a compatibility layer to migrate old flag data into the new event format.
  • Implement durable storage for the event log, e.g., SQLite or RocksDB-backed log with compaction.
  • Harden security: API authentication, authorization, and audit trails.
  • Consider a richer WASM surface: expose an API to subscribe to specific flag updates and to reset evaluator state.

Would you like me to tailor this tutorial to a particular tech stack you’re using (e.g., prefer Python for the API, or you want the WASM evaluator to run in a React app with Vite)? I can also provide a fuller, runnable code scaffold for Rust and Node.js with concrete file structures and complete build scripts.

-

Rizwan Saleem | https://rizwansaleem.co

Top comments (0)