Enterprise-grade feature flags and A/B testing for Forge applications.
- ✅ Multiple Providers - Local, LaunchDarkly, Unleash, Flagsmith
- ✅ Boolean Flags - Simple on/off toggles
- ✅ Multi-Variant Flags - String, number, JSON flags
- ✅ User Targeting - Target specific users or groups
- ✅ Percentage Rollouts - Gradual feature rollouts
- ✅ Real-time Updates - Automatic flag refresh
- ✅ Caching - Local caching for performance
- ✅ A/B Testing - Experimentation support
go get github.com/xraph/forge/extensions/featurespackage main
import (
"github.com/xraph/forge"
"github.com/xraph/forge/extensions/features"
)
func main() {
app := forge.NewApp(forge.AppConfig{
Extensions: []forge.Extension{
features.NewExtension(
features.WithEnabled(true),
features.WithLocalFlags(map[string]features.FlagConfig{
"new-checkout": {
Key: "new-checkout",
Name: "New Checkout Flow",
Description: "Enable new checkout experience",
Type: "boolean",
Enabled: true,
},
"max-cart-items": {
Key: "max-cart-items",
Name: "Maximum Cart Items",
Type: "number",
Value: 100,
},
}),
),
},
})
// Get features service using helper
featuresService := features.MustGetFromApp(app)
// Check if feature is enabled
router := app.Router()
router.POST("/checkout", func(ctx forge.Context) error {
userCtx := features.NewUserContext(ctx.UserID()).
WithEmail(ctx.User().Email)
if featuresService.IsEnabled(ctx.Context(), "new-checkout", userCtx) {
return handleNewCheckout(ctx)
}
return handleLegacyCheckout(ctx)
})
app.Run()
}extensions:
features:
enabled: true
provider: "local"
refresh_interval: 30s
enable_cache: true
cache_ttl: 5m
# Local provider (for development)
local:
flags:
new-checkout:
key: "new-checkout"
name: "New Checkout Flow"
type: "boolean"
enabled: true
targeting:
- attribute: "email"
operator: "contains"
values: ["@company.com"]
value: true
rollout:
percentage: 50
attribute: "user_id"
theme:
key: "theme"
name: "UI Theme"
type: "string"
value: "dark"
# Default flag values
default_flags:
new-checkout: false
max-cart-items: 100features.NewExtension(
features.WithEnabled(true),
features.WithProvider("local"),
features.WithRefreshInterval(30 * time.Second),
features.WithCache(true, 5 * time.Minute),
features.WithLocalFlags(map[string]features.FlagConfig{
// ... flags
}),
)// Check if feature is enabled
userCtx := features.NewUserContext("user-123").
WithEmail("user@example.com").
WithGroups([]string{"beta-testers"})
if service.IsEnabled(ctx, "new-feature", userCtx) {
// New feature enabled
}
// With custom default
enabled := service.IsEnabledWithDefault(ctx, "experimental-feature", userCtx, false)// Get string flag
theme := service.GetString(ctx, "theme", userCtx, "light")
// Use in templates
return ctx.Render("dashboard.html", map[string]interface{}{
"theme": theme,
})// Get number flag
maxItems := service.GetInt(ctx, "max-cart-items", userCtx, 50)
pageSize := service.GetInt(ctx, "page-size", userCtx, 20)// Get complex configuration
config := service.GetJSON(ctx, "api-config", userCtx, map[string]interface{}{
"timeout": 30,
"retry": 3,
})// Get all flags for a user
allFlags, err := service.GetAllFlags(ctx, userCtx)
if err != nil {
return err
}
// Return to frontend
return ctx.JSON(200, map[string]interface{}{
"flags": allFlags,
})flags:
vip-features:
targeting:
- attribute: "email"
operator: "in"
values: ["admin@company.com", "ceo@company.com"]
value: true
- attribute: "group"
operator: "in"
values: ["vip", "enterprise"]
value: trueflags:
new-ui:
rollout:
percentage: 25 # 25% of users
attribute: "user_id" # Consistent hashing on user_idflags:
beta-feature:
targeting:
# Enable for beta testers
- attribute: "group"
operator: "in"
values: ["beta-testers"]
value: true
rollout:
# Plus 10% of regular users
percentage: 10
attribute: "user_id"features.NewExtension(
features.WithProvider("local"),
features.WithLocalFlags(localFlags),
)features.NewExtension(
features.WithLaunchDarkly(os.Getenv("LAUNCHDARKLY_SDK_KEY")),
)features.NewExtension(
features.WithUnleash(
"https://unleash.company.com",
os.Getenv("UNLEASH_API_TOKEN"),
"my-app",
),
)features.NewExtension(
features.WithFlagsmith(
"https://flagsmith.company.com",
os.Getenv("FLAGSMITH_ENV_KEY"),
),
)// Feature flag middleware
func FeatureFlagMiddleware(flagKey string, service *features.Service) forge.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userCtx := features.NewUserContext(getUserID(r))
if !service.IsEnabled(r.Context(), flagKey, userCtx) {
http.Error(w, "Feature not available", http.StatusNotFound)
return
}
next.ServeHTTP(w, r)
})
}
}
// Use in routes
router.Use(FeatureFlagMiddleware("api-v2", featuresService))// ✅ Good
"new-checkout-flow"
"enable-email-notifications"
"max-upload-size"
// ❌ Bad
"flag1"
"new-feature"
"temp"// ✅ Good - provides fallback
enabled := service.IsEnabledWithDefault(ctx, "feature", userCtx, false)
// ❌ Bad - panics if flag not found
enabled := service.IsEnabled(ctx, "feature", userCtx)// ✅ Good - provides targeting context
userCtx := features.NewUserContext(user.ID).
WithEmail(user.Email).
WithGroups(user.Groups).
WithAttribute("plan", user.Plan)
// ❌ Bad - no targeting possible
userCtx := nil// For frequently checked flags in hot paths
var cachedFlag atomic.Bool
go func() {
ticker := time.NewTicker(10 * time.Second)
for range ticker.C {
enabled := service.IsEnabled(ctx, "hot-feature", userCtx)
cachedFlag.Store(enabled)
}
}()// Remove flags after full rollout
// Document flag lifecycle in code comments
// TODO: Remove this flag after 2025-06-01
if service.IsEnabled(ctx, "temp-feature", userCtx) {
// ...
}func TestFeatureFlag(t *testing.T) {
// Create test provider
provider := providers.NewLocalProvider(
features.LocalProviderConfig{
Flags: map[string]features.FlagConfig{
"test-flag": {
Key: "test-flag",
Type: "boolean",
Enabled: true,
},
},
},
nil,
)
service := features.NewService(provider, logger)
userCtx := features.NewUserContext("test-user")
enabled := service.IsEnabled(context.Background(), "test-flag", userCtx)
assert.True(t, enabled)
}- Caching: Enable caching for production (5-minute TTL recommended)
- Refresh Interval: 30 seconds for real-time, 5 minutes for less critical
- User Context: Only include necessary attributes
- Hot Paths: Cache flag values for high-traffic code paths
// Before
const enableNewFeature = true
if enableNewFeature {
// ...
}
// After
if service.IsEnabled(ctx, "new-feature", userCtx) {
// ...
}MIT License - see LICENSE file for details