Feature Flags
Feature flags let you ship code that’s disabled by default and enable it for specific users or globally — without a redeploy. st8 makes this simple with a single document that your app reads at request time.
Define your flags
Section titled “Define your flags”Create a feature_flags document in your namespace:
cat > feature_flags.json <<'EOF'{ "new_checkout": false, "dark_mode": true, "ai_search": false, "max_upload_mb": 10}EOFst8ctl apply -f feature_flags.json \ --namespace myapp/prod \ --message "initial feature flags"Read flags in Go
Section titled “Read flags in Go”package flags
import ( "context" "encoding/json" "sync" "time"
st8 "github.com/geeper-io/st8/client")
// Flags holds the current feature flag values.// Add new fields here as you add new flags.type Flags struct { NewCheckout bool `json:"new_checkout"` DarkMode bool `json:"dark_mode"` AISearch bool `json:"ai_search"` MaxUploadMB int `json:"max_upload_mb"`}
// Client fetches and caches feature flags from st8.type Client struct { st8 st8.Client scope st8.Scope mu sync.RWMutex flags Flags}
func NewClient(serverURL, token, namespace string) *Client { return &Client{ st8: st8.NewHTTP(serverURL, st8.WithToken(token), st8.WithTimeout(2*time.Second)), scope: st8.Scope{Namespace: namespace, Branch: "main"}, flags: Flags{MaxUploadMB: 10}, // safe defaults }}
// Current returns the cached flags — no network call.func (c *Client) Current() Flags { c.mu.RLock() defer c.mu.RUnlock() return c.flags}
// Refresh fetches the latest flags from st8.func (c *Client) Refresh(ctx context.Context) error { result, err := c.st8.Get(ctx, c.scope, 0, "") if err != nil { return err } raw, ok := result.Objects["feature_flags"] if !ok { return nil } var f Flags if err := json.Unmarshal([]byte(raw), &f); err != nil { return err } c.mu.Lock() c.flags = f c.mu.Unlock() return nil}
// Poll refreshes flags every interval until ctx is cancelled.func (c *Client) Poll(ctx context.Context, interval time.Duration) { go func() { t := time.NewTicker(interval) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: _ = c.Refresh(ctx) } } }()}Use flags in your handlers
Section titled “Use flags in your handlers”type Server struct { flags *flags.Client // ...}
func (s *Server) Upload(w http.ResponseWriter, r *http.Request) { f := s.flags.Current() // reads from cache, no network call
maxBytes := int64(f.MaxUploadMB) << 20 r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
// handle upload...}
func (s *Server) Search(w http.ResponseWriter, r *http.Request) { f := s.flags.Current()
if f.AISearch { s.aiSearch(w, r) } else { s.legacySearch(w, r) }}Enable a flag without deploying
Section titled “Enable a flag without deploying”# Enable AI search for everyonecat > feature_flags.json <<'EOF'{ "new_checkout": false, "dark_mode": true, "ai_search": true, "max_upload_mb": 10}EOFst8ctl apply -f feature_flags.json \ --namespace myapp/prod \ --message "enable ai search"Your app picks up the change within seconds (based on the poll interval). No deploy, no restart.
Kill switch
Section titled “Kill switch”The same mechanism works as a kill switch — set enabled: false to immediately disable a misbehaving feature:
st8ctl apply -f feature_flags.json \ --namespace myapp/prod \ --message "kill switch: disable ai search (latency spike)"where feature_flags.json contains:
{ "new_checkout": false, "dark_mode": true, "ai_search": false, "max_upload_mb": 10}Or use rollback if you have a checkpoint:
st8ctl rollback --checkpoint stable --message "rollback: ai search causing latency"Per-environment flags
Section titled “Per-environment flags”Use namespaces to maintain separate flags per environment:
# Staging gets the flag firstcat > feature_flags.json <<'EOF'{"ai_search": true}EOFst8ctl apply -f feature_flags.json --namespace myapp/staging \ --message "enable ai search in staging"
# After validation, enable in productionst8ctl apply -f feature_flags.json --namespace myapp/prod \ --message "enable ai search in production"Startup
Section titled “Startup”func main() { flagClient := flags.NewClient("https://st8.internal", os.Getenv("ST8_TOKEN"), "myapp/prod")
// Block on initial load; fall back to defaults if st8 is unreachable ctx := context.Background() if err := flagClient.Refresh(ctx); err != nil { log.Printf("warning: could not load feature flags: %v", err) }
// Poll every 5 seconds in background flagClient.Poll(ctx, 5*time.Second)
// ... start server}