API Development

Server-Side Flag Evaluation API: Simplifying Feature Flag Access

VisualReading

Discover how FeatBit's new public Flag Evaluation API enables direct server-side feature flag evaluation with sophisticated filtering capabilities, all while maintaining clean architecture across multiple database backends.

TL;DR

FeatBit's new Flag Evaluation API (PR #854) introduces a public REST endpoint that enables server-side feature flag evaluation without requiring SDK integration. The API supports flexible filtering by tags (AND/OR modes) and specific flag keys, making it perfect for backend services, serverless functions, and microservices architectures.

Built with clean architecture principles, the implementation features a database-agnostic service layer that seamlessly handles both MongoDB and PostgreSQL backends, demonstrating how to build maintainable, scalable feature flag infrastructure. The API includes comprehensive input validation, optimized query patterns, and returns detailed evaluation results including variation values and match reasons.

The Challenge

While FeatBit's SDKs provide powerful client-side and server-side flag evaluation capabilities, some use cases demand a more direct approach:

  • Serverless environments where maintaining SDK state is impractical
  • Backend services that need on-demand flag evaluation without persistent connections
  • Microservices architectures requiring lightweight flag access
  • Integration scenarios where REST API access is more appropriate than SDK embedding
  • Batch processing systems that evaluate flags for multiple users or contexts

Customers needed a way to evaluate feature flags directly from their backend systems using standard HTTP requests, with fine-grained control over which flags to evaluate based on tags and keys.

Solution Overview

The Flag Evaluation API introduces a new /api/public/featureflag/evaluate endpoint that accepts a user context and optional filters, then evaluates all matching feature flags and returns their variations.

Server-Side Evaluation

Evaluate feature flags directly from your backend without SDK integration

Flexible Filtering

Filter flags by tags (AND/OR modes) and specific keys for precise control

Multi-Database Support

Works seamlessly with MongoDB and PostgreSQL backends

Example Request

curl --request POST \
  --url http://localhost:5100/api/public/featureflag/evaluate \
  --header 'Authorization: {MY-CLIENT-ENV-SECRET}' \
  --header 'Content-Type: application/json' \
  --data '{
  "user": {
    "keyId": "tester",
    "name": "Tester User"
  },
  "filter": {
    "tags": ["boolean", "dev"],
    "tagFilterMode": "and",
    "keys": ["game-runner"]
  }
}'

Example Response

[
  {
    "key": "game-runner",
    "variation": {
      "id": "ac727073-8cc5-4ec5-9405-59737ea737bb",
      "type": "boolean",
      "value": "false",
      "matchReason": "flag disabled"
    }
  }
]

Key Benefits

  • • No SDK installation or configuration required
  • • Works with any HTTP client in any programming language
  • • Filter flags by tags (AND/OR logic) for precise control
  • • Filter by specific flag keys for targeted evaluation
  • • Returns detailed match reasons for debugging

API Design & Implementation

Request Validation

The API prioritizes security and user experience with comprehensive input validation:

public bool TryValidate(out string validationError)
{
    validationError = string.Empty;

    // Validate user context
    if (User is null || !User.IsValid())
    {
        validationError = "A valid user is required.";
        return false;
    }

    // Validate tag filter mode
    var filterMode = Filter?.TagFilterMode;
    if (!string.IsNullOrWhiteSpace(filterMode))
    {
        var isValidMode = filterMode is TagFilterMode.And or TagFilterMode.Or;
        if (!isValidMode)
        {
            validationError = $"Invalid tag filter mode: {filterMode}. Valid values are 'and' or 'or'.";
            return false;
        }
    }

    return true;
}

This validation approach was refined through code review feedback, with the team adding explicit tag filter mode validationto prevent silent failures when invalid modes are provided. The validation runs before any database queries, saving resources and providing immediate feedback to API consumers.

Controller Design

The controller is lean and focused, delegating business logic to dedicated services:

[HttpPost("evaluate")]
public async Task<IActionResult> EvaluateAsync(EvaluateFlagRequest request)
{
    // Authentication check
    if (!Authenticated)
    {
        return Unauthorized();
    }

    // Input validation
    if (!request.TryValidate(out var validationError))
    {
        return BadRequest(validationError);
    }

    // Get filtered flags from service
    var filter = request.Filter ?? new FeatureFlagFilter();
    var flags = await flagService.GetListAsync(EnvId, filter);

    // Evaluate each flag for the user
    var evalResults = new List<EvalResult>();
    foreach (var flag in flags)
    {
        var variations = flag.GetProperty("variations")
            .Deserialize<Variation[]>(ReusableJsonSerializerOptions.Web)!;

        var scope = new EvaluationScope(flag, request.User!, variations);
        var userVariation = await evaluator.EvaluateAsync(scope);
        var evalResult = new EvalResult(flag, userVariation);

        evalResults.Add(evalResult);
    }

    return Ok(evalResults);
}

The controller follows the single responsibility principle—it handles HTTP concerns (authentication, validation, response formatting) while delegating flag retrieval and evaluation logic to specialized services. This makes the code easy to test and maintain.

Database Abstraction Layer

One of the most impressive aspects of this implementation is how it achieves database provider abstractionwithout sacrificing performance or readability. The FeatureFlagService exposes a single interface but internally routes to MongoDB or PostgreSQL-specific implementations.

MongoDB Implementation

async Task<ICollection<JsonElement>> MongoDbGetAsync()
{
    var mongodb = serviceProvider.GetRequiredService<IMongoDbClient>();
    var collection = mongodb.Database.GetCollection<BsonDocument>("FeatureFlags");

    var filterBuilder = Builders<BsonDocument>.Filter;
    var filters = new List<FilterDefinition<BsonDocument>>
    {
        // Environment filter
        filterBuilder.Eq("envId", new BsonBinaryData(envId, GuidRepresentation.Standard))
    };

    // Tags filter - supports AND/OR modes
    var tags = userFilter.Tags ?? [];
    if (tags.Length > 0)
    {
        var tagsFilter = userFilter.TagFilterMode switch
        {
            TagFilterMode.And => filterBuilder.All("tags", tags),  // All tags must match
            TagFilterMode.Or => filterBuilder.In("tags", tags),    // Any tag matches
            _ => filterBuilder.Empty
        };
        filters.Add(tagsFilter);
    }

    // Keys filter - exact match on flag keys
    var keys = userFilter.Keys ?? [];
    if (keys.Length > 0)
    {
        filters.Add(filterBuilder.In("key", keys));
    }

    var filter = filterBuilder.And(filters);
    var flags = await collection.Find(filter).ToListAsync();
    
    return flags.Select(x => x.ToJsonElement()).ToArray();
}

PostgreSQL Implementation

async Task<ICollection<JsonElement>> PostgresGetAsync()
{
    var dataSource = serviceProvider.GetRequiredService<NpgsqlDataSource>();
    await using var connection = await dataSource.OpenConnectionAsync();

    var sql = "SELECT * FROM feature_flags WHERE env_id = @envId";
    var parameters = new DynamicParameters();
    parameters.Add("envId", envId);

    // Tags filter using PostgreSQL array operators
    var tags = userFilter.Tags ?? [];
    if (tags.Length > 0)
    {
        var tagFilterSql = userFilter.TagFilterMode switch
        {
            TagFilterMode.And => " AND tags @> @tags",  // Contains all tags
            TagFilterMode.Or => " AND tags && @tags",   // Overlaps with tags
            _ => string.Empty
        };

        if (!string.IsNullOrWhiteSpace(tagFilterSql))
        {
            sql += tagFilterSql;
            parameters.Add("tags", tags);
        }
    }

    // Keys filter using ANY operator
    var keys = userFilter.Keys ?? [];
    if (keys.Length > 0)
    {
        sql += " AND key = ANY(@keys)";
        parameters.Add("keys", keys);
    }

    var rows = await connection.QueryAsync(sql, parameters);
    return rows.Select(x => RowSerializer.SerializeFlag((x as IDictionary<string, object>)!))
        .Select(x => JsonSerializer.Deserialize<JsonElement>(x))
        .ToArray();
}

Architecture Highlights

Clean Separation of Concerns

API contracts, services, and persistence layers are clearly separated for maintainability

Easier testing, debugging, and future enhancements

Database Provider Abstraction

Single service interface supports both MongoDB and PostgreSQL through strategy pattern

No code changes needed when switching databases

Optimized Query Performance

Uses native database features (MongoDB $all/$in operators, PostgreSQL @> && operators)

Fast filtering even with thousands of flags

Validation-First Approach

Input validation happens early with clear error messages before any database queries

Better security and user experience

Code Review Insights

During code review, GitHub Copilot identified a potential performance optimization: the PostgreSQL implementation serializes flags to bytes and immediately deserializes them to JsonElement, creating an unnecessary round-trip. While the team chose to defer this optimization, it demonstrates the value of systematic code review in identifying future improvement opportunities.

Customer Impact

This API directly addresses a customer requirement, enabling several important use cases:

Serverless Integration

AWS Lambda, Azure Functions, and Google Cloud Functions can now evaluate flags on-demand without maintaining stateful SDK connections. Simply make an HTTP request when you need flag values, and FeatBit handles the evaluation logic.

Microservices Architecture

Services can query for flags filtered by specific tags, enabling service-level flag organization. For example, a payment service might query only flags tagged with "payment" and "production", reducing noise and improving performance.

Batch Processing

Background jobs and batch processors can evaluate flags for multiple users or contexts without maintaining long-lived SDK connections. The API supports efficient evaluation of specific flag keys, making it perfect for targeted feature rollouts in batch scenarios.

Language-Agnostic Integration

Any language or platform with HTTP client capabilities can now integrate with FeatBit, even if there's no native SDK available. This dramatically lowers the barrier to adoption for teams using diverse technology stacks.

Release Context

This feature was merged into the main branch and is part of FeatBit's continuous improvement of the evaluation server. The implementation follows FeatBit's commitment to providing flexible, enterprise-grade feature flag infrastructure that works with any architecture pattern.

Getting Started

Ready to use the Flag Evaluation API in your applications? Here's how to get started:

1. Get Your Environment Secret

Navigate to your FeatBit environment settings and copy your client environment secret. This will be used in the Authorization header.

2. Structure Your Request

Create a POST request with:

  • user: User context with keyId (required) and optional properties
  • filter.tags: Array of tags to filter flags (optional)
  • filter.tagFilterMode: "and" or "or" for tag matching (optional, defaults to "and")
  • filter.keys: Array of specific flag keys to evaluate (optional)

3. Make the Request

# Evaluate all flags for a user
curl -X POST http://localhost:5100/api/public/featureflag/evaluate \
  -H "Authorization: YOUR_ENV_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"user": {"keyId": "user-123"}}'

# Evaluate flags with specific tags (AND mode)
curl -X POST http://localhost:5100/api/public/featureflag/evaluate \
  -H "Authorization: YOUR_ENV_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "user": {"keyId": "user-123"},
    "filter": {
      "tags": ["production", "payment"],
      "tagFilterMode": "and"
    }
  }'

# Evaluate specific flags by key
curl -X POST http://localhost:5100/api/public/featureflag/evaluate \
  -H "Authorization: YOUR_ENV_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "user": {"keyId": "user-123"},
    "filter": {
      "keys": ["checkout-v2", "payment-provider"]
    }
  }'