Skip to main content
The Foff Go SDK provides a thread-safe client for resolving feature flag values in Go applications. It starts a background goroutine to poll the Foff API, protects the in-memory cache with a sync.RWMutex, and lets multiple goroutines call GetFeatureConfig concurrently without contention. The client integrates with Go’s context package so you can control its lifetime cleanly.

Requirements

Go 1.22.10 or later.

Installation

go get github.com/twospoon/foff-feature-config-go-sdk

Quick start

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/twospoon/foff-feature-config-go-sdk/pkg/client"
	"github.com/twospoon/foff-feature-config-go-sdk/pkg/config"
)

func main() {
	ctx := context.Background()

	// STEP 1: Create appropriate config
	cfg := &config.Config{
		APIKey:          "your-api-key",
		BaseURL:         "https://foff.twospoon.ai/live",
		Scope:           "name-of-your-scope",
		PollingInterval: 30, // seconds
	}

	// STEP 2: Create the client with the config
	c, err := client.NewClient(ctx, cfg)
	if err != nil {
		log.Fatalf("failed to create client: %v", err)
	}
	defer c.Close()

	// STEP 3: Retrieve configs for your created features for given hierarchies
	value := c.GetFeatureConfig("my-feature", []string{"org-1", "team-a", "user-123"})
	fmt.Println(value)
}
Store your API key in an environment variable and read it with os.Getenv("FOFF_API_KEY"). Never hard-code it in source files.

Configuration

Configure the client using the config.Config struct.
FieldTypeRequiredDefaultDescription
APIKeystringYesYour Foff API key.
BaseURLstringYesBase URL of the Foff API. Use https://foff.twospoon.ai/live.
ScopestringYesThe scope to fetch configs for (e.g. production, staging).
PollingIntervaluint32No10How often, in seconds, to poll for config updates.

Validation

NewClient validates the config before creating the client. It returns an error if APIKey, BaseURL, or Scope is empty.

Polling interval normalization

The polling interval is clamped to safe bounds automatically before polling starts.
If you set PollingInterval below 10 seconds, the SDK raises it to 10. If you set it above 3600 seconds (1 hour), the SDK lowers it to 3600. Set it to 0 to disable polling entirely.

Creating a client

c, err := client.NewClient(ctx, cfg)
if err != nil {
    log.Fatal(err)
}
defer c.Close()
NewClient performs the following steps in order:
1

Validate the config

Returns an error if APIKey, BaseURL, or Scope is empty.
2

Normalize the polling interval

Clamps PollingInterval to the range 10–3600 seconds (unless it is 0).
3

Fetch all configs (blocking)

Makes an HTTP GET request to {BaseURL}/api/v1/scopes/{scope}/configs and populates the in-memory cache.
4

Start background polling goroutine

If PollingInterval > 0, starts a goroutine that re-fetches configs on the configured interval. The goroutine respects the context passed to NewClient.
The initial fetch is blocking. If the Foff API is unreachable or returns an error at startup, NewClient returns that error immediately. Check the error and handle it at startup so your process fails fast rather than serving stale or empty configs.

Resolving feature configs

GetFeatureConfig(featureName string, orderedHierarchy []string) interface{}

Returns the config value for a feature, resolved against a hierarchy. The call reads from the in-memory cache — no network request is made.
value := c.GetFeatureConfig("dark-mode", []string{"org-1", "team-a", "user-123"})

Hierarchy resolution algorithm

The SDK resolves from most-specific to least-specific:
  1. It checks the full combination first — org-1 + team-a + user-123.
  2. It then tries shorter prefixes — org-1 + team-a, then org-1.
  3. If no override matches, it returns the feature’s "default" value.
  4. If the feature does not exist at all, it returns nil.
This means you can define overrides at any level of your hierarchy — organisation, team, user, environment, region, service — and the SDK finds the most specific match automatically.

Thread safety

All public methods on Client are safe for concurrent use. The internal cache is protected by a sync.RWMutex, so multiple goroutines can call GetFeatureConfig simultaneously without contention or data races.

Polling

When PollingInterval is greater than 0, the SDK polls GET {BaseURL}/api/v1/scopes/{scope}/configs in the background and replaces the in-memory cache on each successful response. The default polling interval is 10 seconds.
  • Polling errors are silently ignored. The SDK continues serving the last successfully fetched config.
  • The polling goroutine respects the context passed to NewClient. Cancelling that context stops polling.
  • Calling c.Close() also stops the polling goroutine and releases all resources.
Use caseInterval
Near-real-time10s
Standard30–60s
Low-traffic / batch300–600s

Closing the client

Call c.Close() when your application shuts down to stop the background polling goroutine:
c, err := client.NewClient(ctx, cfg)
if err != nil {
    log.Fatal(err)
}
defer c.Close()
Using defer c.Close() immediately after a successful NewClient call is the recommended pattern. It ensures the goroutine is always stopped, even if your main function returns early.