Skip to content

Commit e12fddd

Browse files
maskarbclaude
andauthored
feat: add Unleash feature flag integration to backend (#654)
Adds feature flag evaluation and workspace override management: - Unleash client wrapper with workspace context support - Flag evaluation endpoint with ConfigMap override precedence - Admin endpoints for listing flags and managing overrides - Tag-based filtering for workspace-configurable flags --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f8dbcbc commit e12fddd

10 files changed

Lines changed: 2001 additions & 13 deletions

File tree

components/backend/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@ make deps-verify # Verify dependencies
179179
make check-env # Verify Go, kubectl, docker installed
180180
```
181181

182+
### Feature flags (Unleash)
183+
184+
See [docs/feature-flags](../../docs/feature-flags/README.md) for env vars, handler usage, and examples.
185+
182186
## Architecture
183187

184188
See `CLAUDE.md` in project root for:
@@ -193,6 +197,8 @@ See `CLAUDE.md` in project root for:
193197
- `handlers/sessions.go` - AgenticSession lifecycle, user/SA client usage
194198
- `handlers/middleware.go` - Auth patterns, token extraction, RBAC
195199
- `handlers/helpers.go` - Utility functions (StringPtr, BoolPtr)
200+
- `handlers/featureflags.go` - Feature flag helpers (see docs/feature-flags/)
201+
- `featureflags/featureflags.go` - Unleash client init
196202
- `types/common.go` - Type definitions
197203
- `server/server.go` - Server setup, middleware chain, token redaction
198204
- `routes.go` - HTTP route definitions and registration
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Package featureflags provides optional Unleash-backed feature flag checks for the backend.
2+
// When UNLEASH_URL and UNLEASH_CLIENT_KEY are not set, all flags are disabled (IsEnabled returns false).
3+
package featureflags
4+
5+
import (
6+
"log"
7+
"net/http"
8+
"os"
9+
"strings"
10+
11+
"github.com/Unleash/unleash-go-sdk/v5"
12+
unleashContext "github.com/Unleash/unleash-go-sdk/v5/context"
13+
)
14+
15+
const appName = "ambient-code-backend"
16+
17+
var initialized bool
18+
19+
// Init initializes the Unleash client when UNLEASH_URL and UNLEASH_CLIENT_KEY are set.
20+
// Safe to call multiple times; only initializes once when config is present.
21+
// Call from main after loading env and before starting the server.
22+
func Init() {
23+
url := strings.TrimSpace(os.Getenv("UNLEASH_URL"))
24+
clientKey := strings.TrimSpace(os.Getenv("UNLEASH_CLIENT_KEY"))
25+
if url == "" || clientKey == "" {
26+
return
27+
}
28+
// Ensure URL has a trailing slash for the SDK
29+
if !strings.HasSuffix(url, "/") {
30+
url += "/"
31+
}
32+
unleash.Initialize(
33+
unleash.WithAppName(appName),
34+
unleash.WithUrl(url),
35+
unleash.WithCustomHeaders(http.Header{"Authorization": {clientKey}}),
36+
)
37+
initialized = true
38+
log.Printf("Unleash feature flags enabled (url=%s)", strings.TrimSuffix(url, "/"))
39+
}
40+
41+
// IsEnabled returns true if the named feature flag is enabled.
42+
// When Unleash is not configured, returns false. Safe to call from any handler.
43+
func IsEnabled(flagName string) bool {
44+
if !initialized {
45+
return false
46+
}
47+
return unleash.IsEnabled(flagName)
48+
}
49+
50+
// IsEnabledWithContext returns true if the flag is enabled for the given user context.
51+
// Use for strategies that depend on userId, sessionId, or remoteAddress.
52+
// When Unleash is not configured, returns false.
53+
func IsEnabledWithContext(flagName string, userID, sessionID, remoteAddress string) bool {
54+
if !initialized {
55+
return false
56+
}
57+
ctx := unleashContext.Context{
58+
UserId: userID,
59+
SessionId: sessionID,
60+
RemoteAddress: remoteAddress,
61+
}
62+
return unleash.IsEnabled(flagName, unleash.WithContext(ctx))
63+
}

components/backend/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
toolchain go1.24.7
66

77
require (
8+
github.com/Unleash/unleash-go-sdk/v5 v5.1.0
89
github.com/anthropics/anthropic-sdk-go v1.2.0
910
github.com/gin-contrib/cors v1.7.6
1011
github.com/gin-gonic/gin v1.10.1
@@ -54,6 +55,7 @@ require (
5455
github.com/josharian/intern v1.0.0 // indirect
5556
github.com/json-iterator/go v1.1.12 // indirect
5657
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
58+
github.com/launchdarkly/eventsource v1.10.0 // indirect
5759
github.com/leodido/go-urn v1.4.0 // indirect
5860
github.com/mailru/easyjson v0.7.7 // indirect
5961
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -64,11 +66,13 @@ require (
6466
github.com/pkg/errors v0.9.1 // indirect
6567
github.com/pmezard/go-difflib v1.0.0 // indirect
6668
github.com/spf13/pflag v1.0.6 // indirect
69+
github.com/stretchr/objx v0.5.2 // indirect
6770
github.com/tidwall/gjson v1.18.0 // indirect
6871
github.com/tidwall/match v1.1.1 // indirect
6972
github.com/tidwall/pretty v1.2.1 // indirect
7073
github.com/tidwall/sjson v1.2.5 // indirect
7174
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
75+
github.com/twmb/murmur3 v1.1.8 // indirect
7276
github.com/ugorji/go/codec v1.3.0 // indirect
7377
github.com/x448/float16 v0.8.4 // indirect
7478
go.opencensus.io v0.24.0 // indirect

components/backend/go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykW
88
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
99
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
1010
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
11+
github.com/Unleash/unleash-go-sdk/v5 v5.1.0 h1:W+HHQklU5/H9kjYTn/T4TKvDHE0BxnZ0+MyTk06RdYw=
12+
github.com/Unleash/unleash-go-sdk/v5 v5.1.0/go.mod h1:1u8BfdyjlkV5j43la61n9A9ul4E+YQC2kKQotz8z7BE=
1113
github.com/anthropics/anthropic-sdk-go v1.2.0 h1:RQzJUqaROewrPTl7Rl4hId/TqmjFvfnkmhHJ6pP1yJ8=
1214
github.com/anthropics/anthropic-sdk-go v1.2.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
1315
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
@@ -116,6 +118,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
116118
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
117119
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
118120
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
121+
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
122+
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
123+
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
124+
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
119125
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
120126
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
121127
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -137,6 +143,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
137143
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
138144
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
139145
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
146+
github.com/launchdarkly/eventsource v1.10.0 h1:H9Tp6AfGu/G2qzBJC26iperrvwhzdbiA/gx7qE2nDFI=
147+
github.com/launchdarkly/eventsource v1.10.0/go.mod h1:J3oa50bPvJesZqNAJtb5btSIo5N6roDWhiAS3IpsKck=
148+
github.com/launchdarkly/go-test-helpers/v3 v3.1.0 h1:E3bxJMzMoA+cJSF3xxtk2/chr1zshl1ZWa0/oR+8bvg=
149+
github.com/launchdarkly/go-test-helpers/v3 v3.1.0/go.mod h1:Ake5+hZFS/DmIGKx/cizhn5W9pGA7pplcR7xCxWiLIo=
140150
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
141151
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
142152
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@@ -155,6 +165,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
155165
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
156166
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
157167
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
168+
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
169+
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
158170
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
159171
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
160172
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
@@ -194,6 +206,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
194206
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
195207
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
196208
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
209+
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
210+
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
197211
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
198212
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
199213
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Package handlers: feature flag helpers for use inside HTTP handlers.
2+
// Backed by the optional Unleash integration (see featureflags package).
3+
// When Unleash is not configured, all flags are disabled.
4+
5+
package handlers
6+
7+
import (
8+
"ambient-code-backend/featureflags"
9+
10+
"github.com/gin-gonic/gin"
11+
)
12+
13+
// FeatureEnabled returns true if the named feature flag is enabled (global check).
14+
// When Unleash is not configured, returns false. Safe to call from any handler.
15+
func FeatureEnabled(flagName string) bool {
16+
return featureflags.IsEnabled(flagName)
17+
}
18+
19+
// FeatureEnabledForRequest returns true if the flag is enabled for the current request.
20+
// Uses forwarded user ID, client IP, and optional session for Unleash strategies (e.g. userId, remoteAddress).
21+
// When Unleash is not configured, returns false.
22+
func FeatureEnabledForRequest(c *gin.Context, flagName string) bool {
23+
userID := c.GetString("userID")
24+
sessionID := c.GetHeader("X-Session-Id") // optional
25+
remoteAddr := c.ClientIP()
26+
return featureflags.IsEnabledWithContext(flagName, userID, sessionID, remoteAddr)
27+
}

0 commit comments

Comments
 (0)