Skip to content

DevCycleHQ-Sandbox/flagd-ofrep-cf-worker-experiments

Repository files navigation

flagd OFREP Cloudflare Workers

This repository is an experiment focused on comparing JavaScript and Rust Cloudflare Worker implementations of OFREP (OpenFeature Remote Evaluation Protocol).

The goal is to document and measure:

  • Implementation challenges (runtime compatibility, dependency constraints, and packaging ergonomics)
  • Performance characteristics (single-flag and bulk evaluation behavior)
  • Practical trade-offs between maintainability, portability, and throughput/latency

TLDR: Result

After comparing JavaScript and Rust Cloudflare Worker implementations, we are moving forward with the JavaScript OFREP Worker in the OpenFeature repo.

Why:

  • Rust Workers have slower cold starts in this setup due to the WASM loading/initialization overhead versus JavaScript.
  • Runtime evaluation performance differences were not noticeable enough to justify the Rust complexity for this use case.
  • The JavaScript JSONLogic ecosystem used here is more mature and simpler to operate today.

Outcome:

  • JavaScript Worker is the default path for upstream adoption.
  • This experiment also proves a Rust-based Cloudflare Worker is viable and remains a valid future option as constraints/performance characteristics evolve.

JS flagd-core worker compatibility tasks and required changes:

  1. Add a worker-safe runtime mode
    • Introduce workers?: boolean option on FlagdCore so worker behavior is explicit and opt-in.
  2. Remove runtime code generation paths
    • Switch JSONLogic evaluation from compiled mode (.build()) to interpreter mode (.run()) when workers: true.
  3. Pre-generate schema validators at build time
    • Use ajv-standalone to generate static validators and avoid new Function() during worker startup.
  4. Thread options through core evaluation pipeline
    • Ensure parser/storage/targeting construction propagates worker mode consistently.
  5. Add generated artifacts + build script wiring
    • Check in generated validator module and keep generation script part of build/release workflow.
  6. Validate behavior + performance baseline
    • Add compatibility tests and benchmark checks to confirm OFREP parity and acceptable latency in worker environments.
  7. Upstream rollout steps
    • Land changes in js-sdk-contrib, consume from JS CF worker package, then promote to OpenFeature once stabilized.

Overview

This project enables feature flag evaluation entirely within Cloudflare Workers using the flagd evaluation engine. It includes three experimental implementations:

  • JavaScript Worker: Uses a Workers-compatible fork of @openfeature/flagd-core
  • Rust Worker: Uses the flagd Rust SDK compiled to native WebAssembly
  • Rust Forking Worker: Uses the standalone forking-flagd-evaluator crate (WASM-first design)

All implementations expose the same OFREP API, allowing clients to evaluate flags via HTTP so behavior and performance can be compared with minimal API differences.

┌─────────────────────────────────────────────────────────────────┐
│                     Cloudflare Worker                           │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    OFREP Handler                          │  │
│  │  POST /ofrep/v1/evaluate/flags/{key}  → Single eval       │  │
│  │  POST /ofrep/v1/evaluate/flags        → Bulk eval         │  │
│  └───────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              flagd Evaluation Engine                      │  │
│  │  • JSONLogic targeting rules                              │  │
│  │  • Fractional evaluation (percentage rollouts)            │  │
│  │  • Semantic version comparison                            │  │
│  │  • String operations (starts_with, ends_with)             │  │
│  └───────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                  Flag Configuration                       │  │
│  │             (bundled JSON at build time)                  │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Project Structure

flagd-ofrep-cf-worker/
├── packages/
│   ├── js-ofrep-worker/           # Reusable JS package
│   ├── rust-ofrep-worker/         # Reusable Rust crate (uses rust-sdk-contrib)
│   └── rust-ofrep-worker-forking/ # Reusable Rust crate (uses flagd-evaluator)
├── examples/
│   ├── js-worker/                 # JS Cloudflare Worker example
│   ├── rust-worker/               # Rust Cloudflare Worker example
│   └── rust-worker-forking/       # Rust Cloudflare Worker (forking) example
├── contrib/
│   ├── js-sdk-contrib/            # Git submodule: forked JS flagd-core
│   ├── rust-sdk-contrib/          # Git submodule: forked Rust flagd SDK
│   └── flagd-evaluator/           # Git submodule: forked flagd-evaluator
└── docs/                          # Documentation

JavaScript Worker

The Challenge

The standard @openfeature/flagd-core package cannot run in Cloudflare Workers because it depends on libraries that use dynamic code generation:

Library Usage Problem
ajv JSON Schema validation Uses new Function() to compile validators at module load time
json-logic-engine Targeting rule evaluation Uses eval() in .build() compilation mode

Cloudflare Workers run in V8 isolates with strict security restrictions that block eval() and new Function().

The Solution

This repository includes a fork of @openfeature/flagd-core (as a git submodule) with an optional workers compatibility mode that:

  1. Pre-compiled ajv validators: Generated at build time using ajv-standalone, avoiding runtime code generation
  2. Interpreter mode for JSONLogic: Uses .run() instead of .build(), which interprets rules without code generation

Fork Details

The fork is maintained at DevCycleHQ-Sandbox/js-sdk-contrib on the feat/workers-compatibility branch.

Files modified in libs/shared/flagd-core/:

File Changes
src/lib/options.ts New FlagdCoreOptions interface with workers?: boolean
src/lib/targeting/targeting.ts Conditionally uses .run() interpreter when workers: true
src/lib/parser.ts Uses pre-compiled validators when workers: true
src/lib/feature-flag.ts Passes options through to Targeting
src/lib/storage.ts Passes options through to parser
src/lib/flagd-core.ts Accepts FlagdCoreOptions in constructor
scripts/build-validators.js Generates pre-compiled ajv validators
src/lib/generated/validators.js Auto-generated pre-compiled validators

Direct Usage

import { FlagdCore } from '@openfeature/flagd-core';

// For Cloudflare Workers (no dynamic code generation)
const core = new FlagdCore(undefined, undefined, { workers: true });

// For Node.js (default, uses compilation for better performance)
const core = new FlagdCore();

Performance Trade-off

Mode JSONLogic Performance Environment
workers: false (default) Compiled (.build()) ~10-20x faster Node.js
workers: true Interpreted (.run()) Slower but compatible Cloudflare Workers

For typical OFREP usage (a few flag evaluations per request), the interpreter mode is fast enough. The absolute times are still in microseconds.

Quick Start (JS)

# Install dependencies
npm install

# Build packages
npm run build

# Run the JS worker
npm run dev:js

Package Usage

import { createOfrepHandler } from '@openfeature/flagd-ofrep-cf-worker';
import flags from './flags.json';

const handler = createOfrepHandler({ flags });

export default {
  fetch: handler,
};

See packages/js-ofrep-worker/README.md for full documentation.


Rust Worker

The Challenge

The standard flagd Rust SDK (open-feature-flagd crate) cannot compile to WASM for Cloudflare Workers.

Important clarification: Cloudflare Workers do support async Rust via wasm-bindgen-futures, which bridges Rust Futures to JavaScript Promises. The workers-rs crate uses this automatically. The problem is specifically tokio, not async in general.

The open-feature Rust crate unconditionally depends on tokio, which depends on mio for I/O polling:

open-feature v0.2.7
└── tokio v1.49.0
    └── mio v1.1.1  ← Uses system calls (epoll/kqueue) not available in WASM

Even with --no-default-features, the open-feature crate still pulls in tokio. This is a limitation of the upstream crate design.

Dependency Usage Problem
tokio Async runtime Uses mio for I/O which requires system calls
open-feature crate OpenFeature SDK Unconditionally depends on tokio
mio I/O polling Uses epoll/kqueue/etc. not available in WASM

The Solution

Since we cannot use the open-feature crate at all (it unconditionally pulls in tokio), we created a fork of the flagd Rust SDK with a new wasm feature that bypasses it entirely:

  1. Makes open-feature dependency optional: The core evaluation logic doesn't need the full OpenFeature SDK
  2. Creates WasmEvaluationContext: A lightweight context type replacing open_feature::EvaluationContext
  3. Adds SimpleFlagStore: Synchronous flag evaluation using serde_json::Value directly
  4. Gates async code: All tokio-dependent code is behind #[cfg(feature = "tokio")]

The evaluation logic itself is synchronous (JSONLogic rules, fractional rollouts, etc.), so avoiding tokio doesn't limit functionality - it just requires alternative types for the evaluation context.

Fork Details

The fork is maintained at DevCycleHQ-Sandbox/rust-sdk-contrib on the feat/wasm-support branch.

Key changes to the flagd crate:

File Changes
Cargo.toml Added wasm feature, made open-feature optional
src/lib.rs Gated FlagdProvider, FlagdOptions behind #[cfg(feature = "tokio")]
src/wasm_context.rs New WasmEvaluationContext type for WASM environments
src/resolver/in_process/simple_store.rs New SimpleFlagStore for sync evaluation
src/resolver/in_process/targeting/mod.rs Updated to use WasmEvaluationContext in WASM mode
src/resolver/in_process/model/mod.rs Gated value_converter module
src/error.rs Added FlagNotFound, FlagDisabled error variants

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    Rust Worker (WASM)                           │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              rust-ofrep-worker crate                      │  │
│  │  • OfrepHandler - HTTP request/response handling          │  │
│  │  • OFREP types (Request, Response, Error)                 │  │
│  └───────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │         open-feature-flagd (wasm feature)                 │  │
│  │  • SimpleFlagStore - sync flag evaluation                 │  │
│  │  • WasmEvaluationContext - lightweight context            │  │
│  │  • No tokio, no open-feature crate dependencies           │  │
│  └───────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              Pure Rust Dependencies                       │  │
│  │  • datalogic-rs - JSONLogic evaluation                    │  │
│  │  • murmurhash3 - fractional rollout bucketing             │  │
│  │  • semver - semantic version comparison                   │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Key Benefits

  • Native WASM: Compiles to WebAssembly bytecode, no new Function() restrictions
  • Smaller bundle: ~562 KB gzipped vs JS implementation
  • Type safety: Full Rust type checking at compile time
  • Same flag format: Uses identical flagd JSON configuration

Quick Start (Rust)

Prerequisites:

  • Rust toolchain via rustup
  • wasm32-unknown-unknown target: rustup target add wasm32-unknown-unknown
  • Node.js 25+ (for wrangler)
# Build the Rust worker
cd examples/rust-worker
npx wrangler build

# Run locally
npx wrangler dev --port 8788

Package Usage

use rust_ofrep_worker::{OfrepHandler, OfrepRequest};
use worker::*;

const FLAGS_JSON: &str = include_str!("flags.json");

#[event(fetch)]
async fn main(mut req: Request, _env: Env, _ctx: Context) -> Result<Response> {
    let handler = OfrepHandler::new(FLAGS_JSON)?;
    
    let body: OfrepRequest = req.json().await.unwrap_or_default();
    match handler.evaluate_flag("my-flag", &body) {
        Ok(result) => Response::from_json(&result),
        Err(error) => Response::from_json(&error),
    }
}

See packages/rust-ofrep-worker/README.md for full documentation.


Rust Forking Worker

The Challenge

The forking-flagd-evaluator is a standalone WASM-first evaluator originally designed for embedding in Java applications via Chicory. It provides an alternative to the rust-sdk-contrib approach with a cleaner, purpose-built architecture.

However, it includes a WASM host import for getting the current time (used for $flagd.timestamp context enrichment):

#[link(wasm_import_module = "host")]
extern "C" {
    fn host_get_current_time() -> u64;
}

This causes esbuild to fail when bundling for Cloudflare Workers:

Could not resolve "host"
    index.js:428:25:
      428 │ import * as import1 from "host"

In Chicory, the Java host provides this module. In Cloudflare Workers, there's no equivalent - we need to use JavaScript's Date.now() instead.

The Solution

We maintain a fork that adds a js-time feature flag. When enabled, this feature:

  1. Disables the host import: The extern "C" block is conditionally compiled out
  2. Uses JavaScript time: Calls js_sys::Date::now() instead of the host function

The feature is additive - default builds still work with Chicory, but enabling js-time makes it Cloudflare Workers compatible.

Fork Details

The fork is maintained at DevCycleHQ-Sandbox/flagd-evaluator on the feat/js-time-feature branch.

Key changes:

File Changes
Cargo.toml Added js-time feature, optional js-sys dependency
src/lib.rs Gated host import behind #[cfg(not(feature = "js-time"))]
src/lib.rs Added js_sys::Date::now() path when js-time enabled

Cargo.toml changes:

[features]
default = []
js-time = ["js-sys"]

[dependencies]
js-sys = { version = "0.3", optional = true }

Conditional compilation in lib.rs:

// Host import only for Chicory (non-js-time builds)
#[cfg(all(target_family = "wasm", not(feature = "js-time")))]
#[link(wasm_import_module = "host")]
extern "C" {
    fn host_get_current_time() -> u64;
}

pub fn get_current_time() -> u64 {
    #[cfg(all(target_family = "wasm", feature = "js-time"))]
    { (js_sys::Date::now() / 1000.0) as u64 }
    
    #[cfg(all(target_family = "wasm", not(feature = "js-time")))]
    { unsafe { host_get_current_time() } }
    
    #[cfg(not(target_family = "wasm"))]
    { std::time::SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() }
}

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                 Rust Forking Worker (WASM)                      │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │          rust-ofrep-worker-forking crate                  │  │
│  │  • OfrepHandler - HTTP request/response handling          │  │
│  │  • OFREP types (Request, Response, Error)                 │  │
│  └───────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │         flagd-evaluator (js-time feature)                 │  │
│  │  • FlagEvaluator - standalone evaluation engine           │  │
│  │  • Uses js_sys::Date::now() for timestamps                │  │
│  │  • No OpenFeature SDK dependency                          │  │
│  └───────────────────────────────────────────────────────────┘  │
│                            │                                    │
│                            ▼                                    │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │              Pure Rust Dependencies                       │  │
│  │  • datalogic-rs - JSONLogic evaluation                    │  │
│  │  • murmurhash3 - fractional rollout bucketing             │  │
│  │  • serde_json - JSON parsing                              │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Key Benefits

  • Standalone design: No OpenFeature SDK dependency, purpose-built for WASM
  • Dual-runtime support: Same crate works in Chicory (Java) and Cloudflare Workers
  • Native WASM: Compiles to WebAssembly bytecode, no new Function() restrictions
  • Type safety: Full Rust type checking at compile time
  • Same flag format: Uses identical flagd JSON configuration

For a detailed comparison of the two Rust approaches, see docs/rust-wasm-approaches.md.

Quick Start (Rust Forking)

Prerequisites:

  • Rust toolchain via rustup
  • wasm32-unknown-unknown target: rustup target add wasm32-unknown-unknown
  • Node.js 25+ (for wrangler)
# Build the Rust forking worker
cd examples/rust-worker-forking
npx wrangler build

# Run locally
npx wrangler dev --port 8789

Package Usage

use rust_ofrep_worker_forking::{OfrepHandler, OfrepRequest};
use worker::*;

const FLAGS_JSON: &str = include_str!("flags.json");

#[event(fetch)]
async fn main(mut req: Request, _env: Env, _ctx: Context) -> Result<Response> {
    let handler = OfrepHandler::new(FLAGS_JSON)?;
    
    let body: OfrepRequest = req.json().await.unwrap_or_default();
    match handler.evaluate_flag("my-flag", &body) {
        Ok(result) => Response::from_json(&result),
        Err(error) => Response::from_json(&error),
    }
}

See packages/rust-ofrep-worker-forking/README.md for full documentation.


OFREP API Reference

Both JS and Rust workers expose identical OFREP endpoints:

Evaluate Single Flag

POST /ofrep/v1/evaluate/flags/{key}

Request:

{
  "context": {
    "targetingKey": "user-123",
    "email": "user@example.com",
    "plan": "premium"
  }
}

Response (200):

{
  "key": "my-feature",
  "value": true,
  "reason": "TARGETING_MATCH",
  "variant": "on",
  "metadata": {
    "flagSetId": "production"
  }
}

Bulk Evaluate All Flags

POST /ofrep/v1/evaluate/flags

Request:

{
  "context": {
    "targetingKey": "user-123"
  }
}

Response (200):

{
  "flags": [
    {
      "key": "feature-a",
      "value": true,
      "reason": "STATIC",
      "variant": "on"
    }
  ],
  "metadata": {
    "flagSetId": "production",
    "version": "1.0.0"
  }
}

Test the API

# Health check
curl http://localhost:8787/

# Evaluate a single flag
curl -X POST http://localhost:8787/ofrep/v1/evaluate/flags/simple-boolean \
  -H "Content-Type: application/json" \
  -d '{"context": {"targetingKey": "user-123"}}'

# Evaluate with targeting context
curl -X POST http://localhost:8787/ofrep/v1/evaluate/flags/targeted-boolean \
  -H "Content-Type: application/json" \
  -d '{"context": {"targetingKey": "user-123", "email": "user@openfeature.dev"}}'

# Bulk evaluate all flags
curl -X POST http://localhost:8787/ofrep/v1/evaluate/flags \
  -H "Content-Type: application/json" \
  -d '{"context": {"targetingKey": "user-123"}}'

Supported Targeting Features

Both implementations support all flagd targeting features:

Feature Description Example
JSONLogic rules Complex conditional logic {"if": [{"==": [{"var": "plan"}, "premium"]}, "on", "off"]}
Fractional evaluation Percentage-based rollouts {"fractional": [["control", 50], ["treatment", 50]]}
String comparison starts_with, ends_with {"starts_with": [{"var": "email"}, "admin"]}
Semantic versioning Version comparison {"sem_ver": [{"var": "version"}, ">=", "2.0.0"]}

Flag Configuration

Flags use the flagd flag definition format. See examples/js-worker/src/flags.json for examples.


Comparison: JS vs Rust Implementations

Aspect JavaScript Worker Rust Worker Rust Forking Worker
Bundle size (gzip) ~180 KB ~562 KB ~667 KB
Evaluation engine json-logic-engine (interpreted) datalogic-rs (native) datalogic-rs (native)
Build time Fast (~2s) Slower (~10s) Slower (~10s)
Type safety Runtime Compile-time Compile-time
Dependencies Fork of flagd-core Fork of rust-sdk-contrib Fork of flagd-evaluator
Code generation None (interpreter mode) None (native WASM) None (native WASM)
Upstream origin OpenFeature JS SDK OpenFeature Rust SDK Forking flagd-evaluator

All implementations pass the same OFREP compliance tests and support identical flag configurations.

When to Use Which

  • JavaScript Worker: Best for teams already using TypeScript/JavaScript, fastest build times
  • Rust Worker: Best for teams using the OpenFeature Rust SDK ecosystem
  • Rust Forking Worker: Best if you want a standalone evaluator with minimal dependencies, or are also using the Java/Chicory integration

Roadmap / Future Enhancements

  • JavaScript Worker: Workers-compatible flagd-core fork
  • Rust Worker: WASM-compatible flagd Rust SDK fork
  • Rust Forking Worker: WASM-compatible forking-flagd-evaluator fork
  • Upstream PRs: Contribute Workers/WASM compatibility back to upstream repos
  • Performance Benchmarks: Compare JS vs Rust evaluation performance
  • Cloudflare KV: Load flag configurations from KV at runtime
  • Durable Objects: Real-time flag updates with WebSocket sync
  • External Sync: Fetch flags from external HTTP endpoint
  • Cache API: Cache evaluated results for performance
  • ETag Support: Bulk evaluation caching with ETags

Development

Build All

npm run build

Run JS Worker

npm run dev:js

Run Rust Worker

cd examples/rust-worker
npx wrangler dev --port 8788

Run Rust Forking Worker

cd examples/rust-worker-forking
npx wrangler dev --port 8789

Update Submodules

# JS SDK contrib
cd contrib/js-sdk-contrib
git pull origin feat/workers-compatibility

# Rust SDK contrib
cd contrib/rust-sdk-contrib
git pull origin feat/wasm-support

# flagd-evaluator (forking)
cd contrib/flagd-evaluator
git pull origin feat/js-time-feature

Regenerate JS Pre-compiled Validators

If the flagd JSON schemas change:

cd contrib/js-sdk-contrib/libs/shared/flagd-core
npm run build:validators

Related Projects

Forks Used

These forks add Workers/WASM compatibility features that aren't yet upstream:

License

Apache-2.0

About

JS/Rust Cloudflare Worker experimentation for flagd OFREP

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors