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