> ## Documentation Index
> Fetch the complete documentation index at: https://docs.foff.twospoon.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Foff Go SDK: thread-safe feature flag client for Go

> Install the Foff Go SDK, create a context-aware client with background polling, and resolve feature flag values safely across concurrent goroutines.

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

```bash theme={null}
go get github.com/twospoon/foff-feature-config-go-sdk
```

## Quick start

```go theme={null}
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)
}
```

<Note>
  Store your API key in an environment variable and read it with `os.Getenv("FOFF_API_KEY")`. Never hard-code it in source files.
</Note>

## Configuration

Configure the client using the `config.Config` struct.

| Field             | Type     | Required | Default | Description                                                    |
| ----------------- | -------- | -------- | ------- | -------------------------------------------------------------- |
| `APIKey`          | `string` | Yes      |         | Your Foff API key.                                             |
| `BaseURL`         | `string` | Yes      |         | Base URL of the Foff API. Use `https://foff.twospoon.ai/live`. |
| `Scope`           | `string` | Yes      |         | The scope to fetch configs for (e.g. `production`, `staging`). |
| `PollingInterval` | `uint32` | No       | `10`    | How 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.

<Warning>
  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.
</Warning>

## Creating a client

```go theme={null}
c, err := client.NewClient(ctx, cfg)
if err != nil {
    log.Fatal(err)
}
defer c.Close()
```

`NewClient` performs the following steps in order:

<Steps>
  <Step title="Validate the config">
    Returns an error if `APIKey`, `BaseURL`, or `Scope` is empty.
  </Step>

  <Step title="Normalize the polling interval">
    Clamps `PollingInterval` to the range 10–3600 seconds (unless it is `0`).
  </Step>

  <Step title="Fetch all configs (blocking)">
    Makes an HTTP `GET` request to `{BaseURL}/api/v1/scopes/{scope}/configs` and populates the in-memory cache.
  </Step>

  <Step title="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`.
  </Step>
</Steps>

<Note>
  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.
</Note>

## 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.

```go theme={null}
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.

### Recommended polling intervals

| Use case            | Interval |
| ------------------- | -------- |
| Near-real-time      | 10s      |
| Standard            | 30–60s   |
| Low-traffic / batch | 300–600s |

## Closing the client

Call `c.Close()` when your application shuts down to stop the background polling goroutine:

```go theme={null}
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.
