Dynamic Configuration
Dynamic configuration means your application reads settings from st8 at runtime — not once at startup, but on each request (or on a short polling interval). This lets you change behavior without redeploying.
Basic pattern: fetch on each request
Section titled “Basic pattern: fetch on each request”The simplest approach: fetch the relevant config document on every request.
package config
import ( "context" "encoding/json" "time"
st8 "github.com/geeper-io/st8/client")
type FeatureFlags struct { DarkMode bool `json:"dark_mode"` NewCheckout bool `json:"new_checkout"` MaxPageSize int `json:"max_page_size"`}
type Client struct { st8 st8.Client scope st8.Scope}
func NewClient(serverURL, token string) *Client { return &Client{ st8: st8.NewHTTP(serverURL, st8.WithToken(token), st8.WithTimeout(2*time.Second)), scope: st8.Scope{Namespace: "myapp/prod", Branch: "main"}, }}
func (c *Client) FeatureFlags(ctx context.Context) (FeatureFlags, error) { result, err := c.st8.Get(ctx, c.scope, 0, "") if err != nil { return FeatureFlags{}, err } var flags FeatureFlags if raw, ok := result.Objects["feature_flags"]; ok { _ = json.Unmarshal([]byte(raw), &flags) } return flags, nil}// In your HTTP handlerfunc (h *Handler) Checkout(w http.ResponseWriter, r *http.Request) { flags, err := h.config.FeatureFlags(r.Context()) if err != nil { // fall back to safe defaults — never block the request flags = FeatureFlags{MaxPageSize: 100} }
if !flags.NewCheckout { h.legacyCheckout(w, r) return } h.newCheckout(w, r)}Cached client: poll for changes
Section titled “Cached client: poll for changes”Fetching on every request adds latency. A smarter approach: poll st8 in the background and serve from an in-memory cache.
package config
import ( "context" "encoding/json" "sync" "time"
st8 "github.com/geeper-io/st8/client")
type Config struct { FeatureFlags FeatureFlags `json:"feature_flags"` RateLimits RateLimits `json:"rate_limits"`}
type cachedConfig struct { mu sync.RWMutex value Config lastRev int64 st8 st8.Client scope st8.Scope}
// Get returns the current config from cache — no network call.func (c *cachedConfig) Get() Config { c.mu.RLock() defer c.mu.RUnlock() return c.value}
// Refresh fetches the latest config from st8 if the revision has changed.func (c *cachedConfig) Refresh(ctx context.Context) error { result, err := c.st8.Get(ctx, c.scope, 0, "") if err != nil { return err } if result.Revision == c.lastRev { return nil // nothing changed }
var cfg Config for key, raw := range result.Objects { switch key { case "feature_flags": _ = json.Unmarshal([]byte(raw), &cfg.FeatureFlags) case "rate_limits": _ = json.Unmarshal([]byte(raw), &cfg.RateLimits) } }
c.mu.Lock() c.value = cfg c.lastRev = result.Revision c.mu.Unlock() return nil}
// StartPolling polls st8 every interval and refreshes the cache.func (c *cachedConfig) StartPolling(ctx context.Context, interval time.Duration) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: _ = c.Refresh(ctx) } } }()}// Startupcfg := &cachedConfig{ st8: st8.NewHTTP("https://st8.internal", st8.WithToken(token)), scope: st8.Scope{Namespace: "myapp/prod", Branch: "main"},}if err := cfg.Refresh(ctx); err != nil { log.Printf("warning: could not load initial config: %v", err)}cfg.StartPolling(ctx, 5*time.Second)
// In handler — no network callsflags := cfg.Get().FeatureFlagsPushing config updates
Section titled “Pushing config updates”# Update a single documentcat > feature_flags.json <<'EOF'{"dark_mode": true, "new_checkout": true, "max_page_size": 200}EOFst8ctl apply -f feature_flags.json \ --namespace myapp/prod \ --message "increase page size limit"
# Verify in production within seconds (no deploy)