A production-grade feature flag service built with Hono, TypeScript, PostgreSQL, and Redis.
- Deterministic Rollouts: Hash-based percentage rollout ensures consistent user experience
- Rule-Based Targeting: Fine-grained control with
eq,neq,in,not_in,starts_with,ends_withoperators - Global Kill Switch: Instantly disable features across all environments
- Audit Logging: Track all changes with before/after snapshots and actor information
- Redis Caching: Fast flag evaluation with cache-aside pattern
- Clean Architecture: Pure domain logic, separated concerns, type-safe codebase
┌─────────────────┐
│ Admin API │ ──────────────► Prisma ──────► PostgreSQL
│ (Write Path) │ (Write)
└─────────────────┘ │
│
▼
┌───────────┐
│ Audit Log │
└───────────┘
┌─────────────────┐
│ Public API │ ──────────────► Redis ──────► Domain Evaluator
│ (Read Path) │ (Cache) (Evaluate)
└─────────────────┘ │
▼
PostgreSQL
(On Cache Miss)
- Global Kill Switch - If
enabled: false, immediately return disabled - Targeting Rules - Check explicit allow rules in order
- Percentage Rollout - Use
hash(userId + flagKey) % 100for deterministic rollout - Default Deny - If no match, return disabled
- Runtime: Node.js with TypeScript
- Web Framework: Hono
- Database: PostgreSQL with Prisma ORM
- Cache: Redis (ioredis)
- Testing: Vitest
- Node.js 18+
- PostgreSQL 14+
- Redis 6+
- Clone the repository and install dependencies:
npm install- Set up environment variables:
cp .env.example .envEdit .env with your configuration:
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/feature_flags"
# Redis
REDIS_URL="redis://localhost:6379"
# Admin API Token
ADMIN_TOKEN="your-secret-admin-token"- Set up the database:
# Generate Prisma Client
npx prisma generate
# Run migrations (create tables)
npx prisma migrate dev --name init
# (Optional) Seed sample data
npx prisma db seedDevelopment mode (with hot reload):
npm run devProduction mode:
npm run build
npm startThe service will be available at http://localhost:3000
Evaluate feature flags for a user.
Endpoint: POST /api/evaluate
Headers: None required
Request Body:
{
"environment": "prod",
"user": {
"id": "user-123",
"email": "john@example.com",
"country": "US",
"attributes": {
"plan": "premium",
"role": "admin"
}
}
}Response:
{
"results": [
{
"flagKey": "new_checkout_flow",
"enabled": true,
"reason": "RULE_MATCH"
},
{
"flagKey": "dark_mode",
"enabled": false,
"reason": "NO_MATCH"
}
]
}Example using curl:
curl -X POST http://localhost:3000/api/evaluate \
-H "Content-Type: application/json" \
-d '{
"environment": "prod",
"user": {
"id": "user-123",
"email": "john@example.com",
"country": "US"
}
}'Update a feature flag configuration.
Endpoint: PUT /api/admin/flags/:id
Headers:
x-admin-token: Your admin token from environment variables
Request Body:
{
"enabled": true,
"rolloutPercentage": 50,
"rules": [
{
"field": "email",
"operator": "ends_with",
"value": "@company.com"
},
{
"field": "country",
"operator": "in",
"value": ["US", "CA", "UK"]
}
]
}Response:
{
"flag": {
"id": "clx1234567890",
"key": "new_checkout_flow",
"environment": "prod",
"enabled": true,
"rolloutPercentage": 50,
"rules": [...],
"version": 2,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-02T12:00:00.000Z"
}
}Example using curl:
curl -X PUT http://localhost:3000/api/admin/flags/clx1234567890 \
-H "Content-Type: application/json" \
-H "x-admin-token: your-secret-admin-token" \
-d '{
"enabled": true,
"rolloutPercentage": 50
}'Create a flag that only enables for users with @company.com email:
{
"enabled": true,
"rolloutPercentage": 0,
"rules": [
{
"field": "email",
"operator": "ends_with",
"value": "@company.com"
}
]
}Roll out to 20% of users (deterministic based on user ID):
{
"enabled": true,
"rolloutPercentage": 20,
"rules": []
}Enable for premium users in specific countries:
{
"enabled": true,
"rolloutPercentage": 0,
"rules": [
{
"field": "attributes.plan",
"operator": "eq",
"value": "premium"
},
{
"field": "country",
"operator": "in",
"value": ["US", "CA", "UK"]
}
]
}Immediately disable a feature:
{
"enabled": false,
"rolloutPercentage": 100,
"rules": []
}| Operator | Description | Example |
|---|---|---|
eq |
Equals | { "field": "country", "operator": "eq", "value": "US" } |
neq |
Not equals | { "field": "role", "operator": "neq", "value": "blocked" } |
in |
In list | { "field": "country", "operator": "in", "value": ["US", "CA"] } |
not_in |
Not in list | { "field": "country", "operator": "not_in", "value": ["RU", "CN"] } |
starts_with |
Starts with | { "field": "email", "operator": "starts_with", "value": "admin@" } |
ends_with |
Ends with | { "field": "email", "operator": "ends_with", "value": "@company.com" } |
The service uses a cache-aside pattern:
- Read: Check Redis first, fallback to PostgreSQL on cache miss
- Write: Invalidate cache after successful database update
- TTL: 60 seconds as a safety net
This ensures fast reads while maintaining consistency.
Run unit tests:
npm testRun tests with coverage:
npm run test:coverageSupported environments:
dev- Developmentstaging- Staging/Pre-productionprod- Production
{
id: string; // UUID
key: string; // Unique flag key
environment: Environment; // dev | staging | prod
enabled: boolean; // Global kill switch
rolloutPercentage: number; // 0-100
rules: Rule[]; // Targeting rules
version: number; // Cache version
createdAt: string; // ISO timestamp
updatedAt: string; // ISO timestamp
}{
id: string;
action: string; // e.g., "UPDATE_FLAG"
entity: string; // e.g., "FeatureFlag"
entityId: string;
before: Json; // Before state
after: Json; // After state
actor: string; // Who made the change
createdAt: string; // ISO timestamp
}All errors return a consistent JSON format:
{
"error": "Error message here"
}Common HTTP status codes:
400- Bad Request (validation error)401- Unauthorized (missing/invalid admin token)404- Not Found (flag doesn't exist)500- Internal Server Error
ISC
This project is designed to demonstrate clean backend architecture. Contributions are welcome!