Production-grade, self-hosted feature flag evaluation engine — targeting rules, gradual rollouts, A/B testing, and audit logging. Built on TypeScript/Node.js with Redis caching, PostgreSQL persistence, and Kubernetes-native deployment.
graph TD
Client["Client / SDK"] -->|POST /evaluate| EvalService["eval-service :3001"]
Client -->|CRUD /flags| FlagAPI["flag-api :3000"]
FlagAPI -->|Read/Write| Postgres["PostgreSQL (source of truth)"]
FlagAPI -->|Invalidate cache| Redis["Redis (cache + stream)"]
EvalService -->|Cache lookup/fill| Redis
EvalService -->|Fallback read| Postgres
EvalService -->|XADD eval event| RedisStream["Redis Stream"]
AuditWorker["audit-worker"] -->|XREAD events| RedisStream
AuditWorker -->|INSERT audit records| Postgres
| Service | Port | Description |
|---|---|---|
flag-api |
3000 | Control plane — CRUD for flags and targeting rules |
eval-service |
3001 | High-throughput evaluation engine — Redis-cached, sub-millisecond p99 |
audit-worker |
— | Background worker — consumes Redis Stream, writes structured audit records to PostgreSQL |
# Clone the repo
git clone https://github.com/Djones-qa/feature-flag-engine.git
cd feature-flag-engine
# Start all services with real PostgreSQL + Redis
docker compose up --buildOnce running, try it out:
# Create a flag
curl -s -X POST http://localhost:3000/flags \
-H 'Content-Type: application/json' \
-d '{
"key": "checkout-v2",
"name": "Checkout V2",
"description": "New checkout flow",
"enabled": true,
"rolloutPercentage": 50,
"rules": [
{
"attribute": "plan",
"operator": "eq",
"values": ["enterprise"],
"variant": "on",
"priority": 1
}
],
"defaultVariant": "off"
}' | jq
# Evaluate the flag for an enterprise user (matches targeting rule → "on")
curl -s -X POST http://localhost:3000/flags/checkout-v2/evaluate \
-H 'Content-Type: application/json' \
-d '{"userId": "user-001", "plan": "enterprise"}' | jq
# Evaluate via eval-service directly (high-throughput path)
curl -s -X POST http://localhost:3001/evaluate \
-H 'Content-Type: application/json' \
-d '{"flagKey": "checkout-v2", "context": {"userId": "user-002", "plan": "free"}}' | jq
# Batch evaluation
curl -s -X POST http://localhost:3001/evaluate/batch \
-H 'Content-Type: application/json' \
-d '{
"flagKeys": ["checkout-v2"],
"context": {"userId": "user-003"}
}' | jq
# Get audit trail
curl -s http://localhost:3000/flags/checkout-v2/audit | jq| Method | Path | Description |
|---|---|---|
POST |
/flags |
Create a flag |
GET |
/flags |
List all active flags |
GET |
/flags/:key |
Get flag by key |
PUT |
/flags/:key |
Update flag (toggle, rules, rollout %) |
DELETE |
/flags/:key |
Archive a flag |
POST |
/flags/:key/evaluate |
Evaluate a flag for a user context |
GET |
/flags/:key/audit |
Get audit trail for a flag |
GET |
/health |
Liveness probe |
GET |
/ready |
Readiness probe (Redis + PostgreSQL) |
| Method | Path | Description |
|---|---|---|
POST |
/evaluate |
Evaluate a single flag |
POST |
/evaluate/batch |
Evaluate multiple flags at once |
GET |
/health |
Liveness probe |
- FLAG_DISABLED —
flag.enabled === false→ returnoff - TARGETING_RULE — evaluate rules in ascending priority order; first match wins
- ROLLOUT — hash
userId:flagKey→ bucket [0-99]; serveonifbucket < rolloutPercentage - DEFAULT — return
flag.defaultVariant
All services are configured via environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 / 3001 |
HTTP listen port |
POSTGRES_HOST |
localhost |
PostgreSQL host |
POSTGRES_PORT |
5432 |
PostgreSQL port |
POSTGRES_DB |
featureflags |
Database name |
POSTGRES_USER |
postgres |
Database user |
POSTGRES_PASSWORD |
postgres |
Database password |
REDIS_HOST |
localhost |
Redis host |
REDIS_PORT |
6379 |
Redis port |
REDIS_PASSWORD |
— | Redis password (optional) |
- Node.js 20+
- npm 10+
- Docker (for integration tests)
# Install all dependencies
npm ci
# Build all packages
npm run build
# Run unit tests (with property-based tests)
npm test
# Run unit tests with coverage
npm run test:coverage
# Typecheck all packages
npm run typecheckfeature-flag-engine/
├── packages/
│ ├── shared/ # Shared types, evaluation engine, DB + cache layers
│ │ ├── src/
│ │ │ ├── types.ts # Core interfaces
│ │ │ ├── evaluator.ts # Targeting rule operator logic
│ │ │ ├── bucketer.ts # Rollout bucketing (MurmurHash)
│ │ │ ├── engine.ts # Full evaluation pipeline
│ │ │ ├── db/ # PostgreSQL repositories
│ │ │ ├── cache/ # Redis flag cache
│ │ │ └── stream/ # Audit stream producer
│ ├── flag-api/ # Control plane API (port 3000)
│ ├── eval-service/ # Evaluation engine (port 3001)
│ ├── audit-worker/ # Redis Stream → PostgreSQL worker
│ └── integration-tests/# testcontainers-based integration tests
├── k8s/ # Kubernetes manifests
├── .github/workflows/ # GitHub Actions CI
└── docker-compose.yml
Integration tests require Docker:
npm run test --workspace=packages/integration-tests# Apply all manifests to a cluster
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/rbac.yaml
# Create the postgres credentials secret
kubectl create secret generic postgres-credentials \
--namespace=feature-flags \
--from-literal=username=postgres \
--from-literal=password=<your-password>
kubectl apply -f k8s/The test suite covers:
- Property-based tests (fast-check): targeting operator contracts, bucketer determinism and uniformity, disabled flag short-circuit, evaluation exhaustiveness, batch consistency, cache round-trips
- Unit tests (Jest): all CRUD endpoints, error paths, audit worker event processing, stream producer
- Integration tests (testcontainers): full evaluation pipeline with real Redis + PostgreSQL, cache invalidation, audit worker end-to-end
MIT © 2024 Darrius Jones
Darrius Jones
GitHub: @Djones-qa
LinkedIn: darrius-jones-28226b350