greenflags

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 9 Imported by: 0

README

greenflags-go

Official Go SDK for consuming GreenFlags feature flags from any Go service: HTTP servers, gRPC services, CLIs, workers.

Zero dependencies — standard library only. Built to minimize billable requests: one network call fetches the whole environment; every read after that is served from memory. Safe for concurrent use across goroutines.

Status: v0.1.0. Full changelog in CHANGELOG.md.

go get github.com/greenflags-dev/greenflags-go

Table of Contents


Why it exists

GreenFlags exposes a read endpoint (GET /v1/flags) where every 2xx response counts as a billable read. A naive integration that fetches a flag on every request handler can generate thousands of unnecessary requests and burn through your quota for no reason.

greenflags-go solves this with a snapshot + cache model:

  1. One call (Refresh) fetches every flag in the project + environment tied to your API token.
  2. That response is stored in memory.
  3. Every read after that (IsEnabled, BoolFlag, StringFlag, …) is local — zero additional requests.

There is intentionally no method to fetch a single flag over the network — that would break the billing model.

Features

  • ✅ Zero dependencies — net/http and the standard library, nothing else.
  • ✅ Snapshot + in-memory cache — billing-safe by design.
  • ✅ Concurrency-safe — share one *Client across goroutines (sync.RWMutex inside).
  • context.Context on every network call — timeouts and cancellation the Go way.
  • ✅ Opt-in polling on a background goroutine — you decide if and how often it refreshes.
  • ✅ Fail-open — a failed refresh keeps the last good snapshot; typed accessors return your defaults.
  • ✅ Client-side geofence evaluation — the end-user's location never leaves your process.
  • ✅ Injectable *http.Client — custom timeouts, proxies, or httptest mocking.

Requirements

  • Go 1.22+.
  • A GreenFlags API token, generated from the dashboard for a specific project + environment. The token determines which flags the SDK sees. See the API docs for the full contract.

Quick Start

package main

import (
	"context"

	greenflags "github.com/greenflags-dev/greenflags-go"
)

func main() {
	flags := greenflags.NewClient(greenflags.Options{
		URL:      "https://app.greenflags.dev",
		APIToken: "gf_your_token_here",
	})

	if err := flags.Refresh(context.Background()); err != nil {
		// fail-open: keep going with defaults
	}

	if flags.IsEnabled("new-checkout") {
		// ship it
	}
}

Usage Guide

flags := greenflags.NewClient(greenflags.Options{URL: url, APIToken: token})

// 1. Fetch the initial snapshot (required before reading real flag values)
if err := flags.Refresh(ctx); err != nil { /* log it */ }

// 2. Read flags — always from memory, never hits the network
enabled := flags.IsEnabled("my-feature")             // bool sugar
theme   := flags.StringFlag("theme", "light")        // string with default
limit   := flags.NumberFlag("rate-limit", 100)       // float64 with default
conf    := flags.JSONFlag("config", nil)             // map[string]any with default
value, ok := flags.GetFlag("anything")               // raw value + existence

// 3. List everything available
all      := flags.AllFlags()   // []Flag
snapshot := flags.Snapshot()   // map[string]Flag

// 4. Subscribe to updates (fires on every successful Refresh)
unsubscribe := flags.Subscribe(func(snapshot map[string]greenflags.Flag) {
	// react to changes
})
defer unsubscribe()

// 5. Opt-in polling — without this, the SDK NEVER fetches data on its own
flags.StartPolling(ctx, 60*time.Second) // every tick = 1 billable request
defer flags.StopPolling()
Ground rules
  • Typed accessors (BoolFlag, StringFlag, NumberFlag, JSONFlag) never panic or error — they return your default when the flag is missing or holds a different type.
  • Refresh can fail (*greenflags.Error: network error, invalid token, quota exceeded) — the previous snapshot is kept either way.
  • Create one client per process and share it — don't build a client (or call Refresh) per request. Refresh once at startup and use StartPolling only if you need near-live data.
  • JSON numbers arrive as float64 (standard encoding/json behavior) — that's what NumberFlag returns.

API Reference

NewClient(Options) *Client
Option Type Description
URL string API base URL, trailing slash optional.
APIToken string Environment-scoped token (gf_...).
Coordinates *Coordinates Optional end-user location for geofence evaluation.
HTTPClient *http.Client Optional override; defaults to a 10s-timeout client.
Methods
Method Signature Description
Refresh (ctx) error 1 request to GET /v1/flags. Replaces the snapshot and notifies subscribers.
Snapshot () map[string]Flag Copy of the evaluated snapshot. Local read.
AllFlags () []Flag Every evaluated flag. Local read.
GetFlag (key) (any, bool) Evaluated value + whether the flag exists.
IsEnabled (key) bool true only when the flag exists and evaluates to true.
BoolFlag / StringFlag / NumberFlag / JSONFlag (key, def) T Typed accessors with defaults. Never error.
Subscribe (fn) (unsubscribe func()) Listener on every successful Refresh.
StartPolling (ctx, interval) Background Refresh loop. Opt-in. Fail-open.
StopPolling () Stops the loop; snapshot preserved.
SetCoordinates (*Coordinates) Sets/clears the location for geofence evaluation. No network.

Geofence

Some flags carry an optional geofence (latitude/longitude/radius, configured in the dashboard). With coordinates set, each geofenced flag is evaluated locally — coordinates never leave the process:

  • Inside the radius (on-edge counts as inside): the flag's normal value.
  • Outside: the off value — false for boolean flags, nil for the rest.
  • No coordinates, or no geofence: the normal value, unaffected.

Fail-open, by design: a geofence is not a security boundary — any caller that omits coordinates sees the flag's normal value regardless of location.

flags.SetCoordinates(&greenflags.Coordinates{Latitude: 19.4326, Longitude: -99.1332})
flags.IsEnabled("store-promo") // evaluated against the geofence, if any
flags.SetCoordinates(nil)      // back to "ignore geofence"

Error Handling

Refresh returns a *greenflags.Error with Code, Message and HTTP Status:

var gfErr *greenflags.Error
if err := flags.Refresh(ctx); errors.As(err, &gfErr) {
	log.Printf("flags refresh failed: %s (%d)", gfErr.Code, gfErr.Status)
}

Codes that GET /v1/flags can actually return:

Code Status Cause
INVALID_TOKEN 401 Token missing, invalid, or revoked
QUOTA_EXCEEDED 429 Monthly read quota exhausted
BILLING_NO_SUBSCRIPTION 429 The workspace has no active subscription
BILLING_CANCELED 429 Subscription canceled
BILLING_PAST_DUE 429 Payment past due
BILLING_TRIAL_EXPIRED 429 Trial expired
BILLING_LIMIT_REACHED 429 Billing limit reached
NETWORK_ERROR 0 The request failed before a response was received
PARSE_ERROR response status Body wasn't valid JSON, or was missing data.flags
REQUEST_ERROR response status Non-2xx response with no parseable error code

Read methods never return any of these — they're always local.

Billing Model

Every Refresh (manual or polling tick) is exactly one HTTP request, and every 2xx response counts as one billable read. All flag reads are 100% in memory. With N instances each polling, you pay N reads per tick — size your interval and instance count accordingly.

Handling the API token

Server-side Go: treat the token like a database password.

  • Read it from the environment (os.Getenv("GREENFLAGS_API_TOKEN")), never hardcode it.
  • Per-token monthly quotas (dashboard → API Tokens) cap the blast radius of a leaked CI log or misconfigured staging.

Development

cd sdks/go
go vet ./...
go test ./...   # 10 tests against httptest servers — no real network

Versioning

Semver via git tags (v0.1.0, …). While in v0.x, MINOR can include API changes; PATCH are fixes. Version-by-version detail in CHANGELOG.md.

Documentation

Overview

Package greenflags is the official Go SDK for GreenFlags feature flags.

Snapshot + cache model: one network call (Refresh) fetches every flag of the token's environment; every read after that is served from memory. See https://greenflags.dev/docs/ for the API reference.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client reads GreenFlags feature flags. It is safe for concurrent use: one Client can be shared across goroutines. Every read path goes through geofence evaluation — the raw cached snapshot is never exposed.

func NewClient

func NewClient(opts Options) *Client

NewClient builds a Client. It performs no network request — call Refresh to load the first snapshot.

func (*Client) AllFlags

func (c *Client) AllFlags() []Flag

AllFlags returns every evaluated flag. Local read.

func (*Client) BoolFlag

func (c *Client) BoolFlag(key string, def bool) bool

BoolFlag returns the flag's value as a bool, or def when the flag is missing or not a bool.

func (*Client) GetFlag

func (c *Client) GetFlag(key string) (any, bool)

GetFlag returns the evaluated value of key and whether the flag exists. Local read; never errors for missing flags.

func (*Client) IsEnabled

func (c *Client) IsEnabled(key string) bool

IsEnabled returns true only when key exists and currently evaluates to true.

func (*Client) JSONFlag

func (c *Client) JSONFlag(key string, def map[string]any) map[string]any

JSONFlag returns the flag's value as a map, or def when the flag is missing or not a JSON object.

func (*Client) NumberFlag

func (c *Client) NumberFlag(key string, def float64) float64

NumberFlag returns the flag's value as a float64 (JSON numbers), or def when the flag is missing or not a number.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context) error

Refresh performs one billable request (GET /v1/flags), replaces the local snapshot and notifies subscribers. On error the previous snapshot is kept.

func (*Client) SetCoordinates

func (c *Client) SetCoordinates(coords *Coordinates)

SetCoordinates sets (or clears, with nil) the end-user location used for geofence evaluation. No network request is made.

func (*Client) Snapshot

func (c *Client) Snapshot() map[string]Flag

Snapshot returns a copy of the evaluated snapshot, keyed by flag key. Local read — never hits the network.

func (*Client) StartPolling

func (c *Client) StartPolling(ctx context.Context, interval time.Duration)

StartPolling refreshes every interval on a background goroutine until StopPolling is called or ctx is canceled. Errors are swallowed: the previous snapshot stays available. Every tick is one billable request.

func (*Client) StopPolling

func (c *Client) StopPolling()

StopPolling stops the polling goroutine. The in-memory snapshot is kept.

func (*Client) StringFlag

func (c *Client) StringFlag(key string, def string) string

StringFlag returns the flag's value as a string, or def when the flag is missing or not a string.

func (*Client) Subscribe

func (c *Client) Subscribe(fn func(map[string]Flag)) (unsubscribe func())

Subscribe registers fn, called with the evaluated snapshot after every successful Refresh. It returns an unsubscribe function.

type Coordinates

type Coordinates struct {
	Latitude  float64
	Longitude float64
}

Coordinates is a geographic point in decimal degrees.

type Error

type Error struct {
	Code    string
	Message string
	Status  int
}

Error is returned on network or API failures. Code mirrors the API error codes (e.g. INVALID_TOKEN, QUOTA_EXCEEDED) plus the client-side NETWORK_ERROR and PARSE_ERROR. Status is the HTTP status (0 for network failures).

func (*Error) Error

func (e *Error) Error() string

type Flag

type Flag struct {
	Key      string    `json:"key"`
	Type     FlagType  `json:"type"`
	Value    any       `json:"value"`
	Geofence *Geofence `json:"geofence,omitempty"`
}

Flag is a feature flag as served by GET /v1/flags. Value holds bool, string, float64, map[string]any or nil depending on Type (and geofence evaluation).

type FlagType

type FlagType string

FlagType is the value type of a flag: "boolean", "string", "number" or "json".

const (
	FlagTypeBoolean FlagType = "boolean"
	FlagTypeString  FlagType = "string"
	FlagTypeNumber  FlagType = "number"
	FlagTypeJSON    FlagType = "json"
)

type Geofence

type Geofence struct {
	Latitude     float64 `json:"latitude"`
	Longitude    float64 `json:"longitude"`
	RadiusMeters float64 `json:"radiusMeters"`
}

Geofence is the geographic scope of a flag value: inside the radius the flag keeps its value, outside it evaluates to its off value.

type Options

type Options struct {
	// URL is the API base URL (e.g. https://app.greenflags.dev), trailing
	// slash optional.
	URL string
	// APIToken is the environment-scoped token (gf_...) created in the
	// dashboard. It determines which flags the client sees.
	APIToken string
	// Coordinates optionally sets the end-user location used for geofence
	// evaluation (see SetCoordinates).
	Coordinates *Coordinates
	// HTTPClient optionally overrides the HTTP client (custom timeouts,
	// proxies, or mocking). Defaults to a client with a 10s timeout.
	HTTPClient *http.Client
}

Options configures a Client. URL and APIToken are required.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL