Skip to content

iamrahulroyy/feature-flag-service

Repository files navigation

Feature Flag Service

A production-grade feature flag service built with Hono, TypeScript, PostgreSQL, and Redis.

Features

  • 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_with operators
  • 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

Architecture

┌─────────────────┐
│   Admin API     │  ──────────────►  Prisma  ──────►  PostgreSQL
│  (Write Path)   │                      (Write)
└─────────────────┘                              │
                                                  │
                                                  ▼
                                           ┌───────────┐
                                           │ Audit Log │
                                           └───────────┘

┌─────────────────┐
│   Public API    │  ──────────────►  Redis  ──────►  Domain Evaluator
│  (Read Path)    │       (Cache)            (Evaluate)
└─────────────────┘                              │
                                                  ▼
                                           PostgreSQL
                                           (On Cache Miss)

Flag Evaluation Order

  1. Global Kill Switch - If enabled: false, immediately return disabled
  2. Targeting Rules - Check explicit allow rules in order
  3. Percentage Rollout - Use hash(userId + flagKey) % 100 for deterministic rollout
  4. Default Deny - If no match, return disabled

Tech Stack

  • Runtime: Node.js with TypeScript
  • Web Framework: Hono
  • Database: PostgreSQL with Prisma ORM
  • Cache: Redis (ioredis)
  • Testing: Vitest

Prerequisites

  • Node.js 18+
  • PostgreSQL 14+
  • Redis 6+

Installation

  1. Clone the repository and install dependencies:
npm install
  1. Set up environment variables:
cp .env.example .env

Edit .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"
  1. 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 seed

Running the Service

Development mode (with hot reload):

npm run dev

Production mode:

npm run build
npm start

The service will be available at http://localhost:3000

API Endpoints

Public API: Evaluate Flags

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"
    }
  }'

Admin API: Update Flag

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
  }'

Usage Examples

Example 1: Enable Feature for Specific Users

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"
    }
  ]
}

Example 2: Gradual Rollout

Roll out to 20% of users (deterministic based on user ID):

{
  "enabled": true,
  "rolloutPercentage": 20,
  "rules": []
}

Example 3: Combined 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"]
    }
  ]
}

Example 4: Global Kill Switch

Immediately disable a feature:

{
  "enabled": false,
  "rolloutPercentage": 100,
  "rules": []
}

Rule Operators

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" }

Cache Strategy

The service uses a cache-aside pattern:

  1. Read: Check Redis first, fallback to PostgreSQL on cache miss
  2. Write: Invalidate cache after successful database update
  3. TTL: 60 seconds as a safety net

This ensures fast reads while maintaining consistency.

Testing

Run unit tests:

npm test

Run tests with coverage:

npm run test:coverage

Environment Types

Supported environments:

  • dev - Development
  • staging - Staging/Pre-production
  • prod - Production

Data Model

FeatureFlag

{
  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
}

AuditLog

{
  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
}

Error Handling

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

License

ISC

Contributing

This project is designed to demonstrate clean backend architecture. Contributions are welcome!

About

A production-grade feature flag service built with Hono, TypeScript, PostgreSQL, and Redis.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors