Documentation

flagctl is a GitOps-native, OpenFeature-compatible feature flag platform. Define flags as code, deploy via PR, and evaluate in milliseconds — no network hops at runtime.

Quickstart

Get your first flag evaluated in production in under 5 minutes.

1

Create an account

Sign up at app.flagctl.io. You'll be prompted to create an organization and name your first workspace. The workspace slug becomes your API subdomain (e.g. acme.flagctl.io).

2

Create an environment

Go to Environments and create one — for example production. Each environment has a slug used in API calls and SDK configuration.

3

Create your first flag

Go to Flags → New flag. Give it a key like checkout-v2, set the type to boolean, and enable it in your environment at 100% rollout.

4

Generate an API key

Go to Settings → API Keys → New key. Choose type server for backend services. Copy the key — it starts with fctl_sk_.

5

Install the SDK and evaluate

See the Java SDK or Spring Boot Starter section below. Within seconds of calling getBooleanValue, you'll see the evaluation in the analytics dashboard.

💡
Flag manifests are cached locally by the SDK and refreshed via polling (default 30 s). Evaluation never blocks on a network call — P99 is under 5 ms.

Core concepts

flagctl is built around a small set of primitives that compose together.

Workspaces

A workspace is your top-level tenant. It maps to a subdomain on flagctl.io. All resources — flags, environments, API keys, audit logs, members — are scoped to a single workspace.

Slug rules

Workspace slugs follow RFC 1123 DNS label rules:

  • Lowercase alphanumeric characters and hyphens only
  • 3–63 characters
  • Cannot start or end with a hyphen

Once set, the slug cannot be changed. Your API and SDK base URL will be https://<slug>.flagctl.io.

Multi-tenancy

flagctl's Cloudflare Worker intercepts all *.flagctl.io traffic, extracts the workspace slug from the subdomain, and injects it as an HTTP header before routing to the API. Your SDK and CLI never parse the subdomain themselves — they just hit your workspace URL.

Environments

Environments represent isolated deployment contexts for your flags — typically production, staging, and development.

Each flag has an independent record per environment:

  • enabled — whether the flag is on in this environment
  • rollout — percentage of users who receive the active value (0–100)
  • default value — value served when the flag is disabled or the user is outside the rollout bucket

Production flag

One environment per workspace can be designated as production. Production environments have stricter controls: kill switch arming requires typed confirmation, and certain mutations are audit-logged with higher severity.

Environment slugs

Every environment has a slug (e.g. production, staging) used in:

  • The Manifest API: GET /api/v1/manifest/{envSlug}
  • SDK and starter configuration: flagctl.environment: production
  • CLI context: flagctl context set --environment production

Flag types

Every flag has a type that determines the shape of its value. The type is fixed at creation and cannot be changed.

Type OpenFeature method Example values
boolean getBooleanValue() true, false
string getStringValue() "dark", "variant_b"
integer getIntegerValue() 100, 5000
double getDoubleValue() 0.5, 1.25
ℹ
The flag's default value is what the SDK returns when the flag is disabled, archived, the user is outside the rollout percentage, or evaluation fails for any reason. It is stored as a string internally and parsed to the flag's declared type at evaluation time.

Lifecycle stages

Lifecycle stages communicate intent and help teams track which flags need cleanup.

Stage Description Typical action
experiment A/B test or controlled rollout. Temporary by nature. Delete after rollout is complete and data is analyzed.
release Gating a feature during a phased release. Remove the flag from code once fully rolled out.
operational Long-lived operational control. Expected to persist. Keep indefinitely. Document owner and purpose.
kill-switch Emergency shut-off for a critical path. Armed by default. Review kill switch drills quarterly.

Status vs lifecycle

Status describes the operational state: active, inactive, or archived. Archived flags are excluded from the manifest and never evaluated. Lifecycle describes the intent and expected lifespan.

Evaluation

Flag evaluation happens entirely in the SDK against a locally cached manifest. There is no network call at evaluation time.

Evaluation order

  1. If the flag key is not found in the manifest → return default value with reason ERROR / FLAG_NOT_FOUND
  2. If killSwitchArmed is true and the flag is not enabled → return default value with reason DISABLED
  3. If the flag is not enabled → return default value with reason DISABLED
  4. If rollout < 100 and a targeting key (user ID) is provided → bucket the user deterministically. Users outside the bucket receive the default value with reason DEFAULT
  5. Otherwise → return flag.defaultValue with reason DEFAULT

Evaluation reasons

ReasonMeaning
DEFAULTFlag is enabled; value comes from the flag's configured default value.
DISABLEDFlag is disabled or kill switch has been activated.
ERROREvaluation could not proceed. Check getErrorCode() for details.

Manifest caching

On startup, the SDK fetches GET /api/v1/manifest/{envSlug} and stores it in memory. A background daemon thread polls this endpoint every N seconds (default 30, minimum 5). The API returns a 304 Not Modified when the manifest hasn't changed (via ETag), so network traffic is minimal at steady state.

If polling fails (network error, transient 5xx), the SDK logs a warning and continues serving the stale cached manifest. Your service never goes down because of a flagctl outage.

Targeting & rollout

Percentage rollout lets you gradually roll out a flag to a fraction of your users while keeping the rest on the default value.

How bucketing works

flagctl uses MurmurHash3 (x86-32) to assign each user to a bucket 0–99. The bucket key is "{flagKey}:{userId}". If bucket < rolloutPct, the user is in the rollout.

This means:

  • Bucketing is deterministic — the same user always gets the same value for the same flag
  • It is independent per flag — user 42 might be in the bucket for checkout-v2 but not for new-pricing
  • Changing the rollout percentage from 20% to 30% adds new users but does not reassign existing ones
java
// Pass a targeting key to participate in rollout bucketing
MutableContext ctx = new MutableContext(user.getId());
ctx.add("plan", user.getPlan());

boolean enabled = flags.getBooleanValue("checkout-v2", false, ctx);
⚠
If no targeting key is provided (anonymous evaluation), rollout bucketing is skipped and the flag evaluates as if it is at 0% rollout — the default value is returned.

Segments

Segments are reusable user groups you define once and reference across many flags. A segment specifies attribute-based rules (e.g. plan == "enterprise"). When a flag targets a segment, users matching that segment receive the flag's enabled value regardless of the rollout percentage.

Segments are managed under Segments in the sidebar of the flagctl dashboard.

Kill switches

A kill switch is a flag in the kill-switch lifecycle stage with killSwitchArmed: true. When armed, your on-call team can disable the feature in production in one click — without a deploy.

Arming a kill switch

Setting a flag's lifecycle to kill-switch and enabling the kill switch arm toggles killSwitchArmed to true in the manifest. The flag continues to serve its enabled value until explicitly activated (shut off).

Activating (firing) the kill switch

To activate, go to the flag detail page and click Activate kill switch. You must type CONFIRM to proceed. This sets the flag to disabled, triggering a manifest update that propagates to all SDK instances within one polling interval (≤30 s by default).

The action is recorded in the audit log with actor identity, timestamp, and the reason field you provide.

Zero Standing Privilege

Kill switch activation requires the typed confirmation to prevent accidental clicks. Only members with the operator or admin role can arm or activate kill switches. Every activation creates an immutable audit record — a requirement for SOC 2 incident response evidence.

ℹ
SSE (Server-Sent Events) is not yet available in the SDK — SDKs poll the manifest. A kill switch propagates within one full poll interval of all connected SDKs.

Java SDK

The flagctl Java SDK implements the OpenFeature FeatureProvider interface. It fetches and caches the flag manifest, evaluates flags locally, and polls for updates in the background.

Requirements

  • Java 17+
  • Maven (included wrapper in the repo)

Installation

The SDK is published to GitHub Packages. Add the repository to your pom.xml:

xml — pom.xml
<repositories>
  <repository>
    <id>github</id>
    <url>https://maven.pkg.github.com/cloudwalkersinc/flagctl-app</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>io.flagctl</groupId>
    <artifactId>flagctl-sdk-java</artifactId>
    <version>0.1.0</version>
  </dependency>
</dependencies>

GitHub Packages requires authentication. Add a Personal Access Token (PAT) with read:packages scope to ~/.m2/settings.xml:

xml — ~/.m2/settings.xml
<settings>
  <servers>
    <server>
      <id>github</id>
      <username>YOUR_GITHUB_USERNAME</username>
      <password>YOUR_PAT</password>
    </server>
  </servers>
</settings>

Initialization

Create a FlagctlProvider once at application startup and register it with OpenFeature:

java
import io.flagctl.sdk.FlagctlSdk;
import io.flagctl.sdk.FlagctlProvider;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Client;

// Build the provider — validates config and starts polling
FlagctlProvider provider = FlagctlSdk.builder()
    .baseUrl("https://acme.flagctl.io")     // your workspace URL
    .apiKey("fctl_sk_...")                 // server API key
    .environment("production")             // environment slug
    .pollIntervalSeconds(30)               // optional, default 30, min 5
    .build();

// Register as global OpenFeature provider and block until the first manifest is fetched
OpenFeatureAPI.getInstance().setProviderAndWait(provider);

// Obtain a client — use this everywhere
Client flags = OpenFeatureAPI.getInstance().getClient();

Evaluating flags

java
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.FlagEvaluationDetails;

// Build evaluation context — the targeting key drives rollout bucketing
MutableContext ctx = new MutableContext(user.getId());
ctx.add("plan", user.getPlan());

// Boolean flag
boolean checkoutV2 = flags.getBooleanValue("checkout-v2", false, ctx);

// String flag
String theme = flags.getStringValue("ui.theme", "light", ctx);

// Integer flag
int limit = flags.getIntegerValue("rate.limit", 1000, ctx);

// Inspect evaluation details
FlagEvaluationDetails<Boolean> details =
    flags.getBooleanDetails("checkout-v2", false, ctx);

details.getValue();         // resolved value
details.getReason();        // "DEFAULT", "DISABLED", "ERROR"
details.getErrorCode();     // non-null only on error
details.getErrorMessage();  // human-readable error

Shutdown

Call OpenFeatureAPI.getInstance().shutdown() during application shutdown to stop the polling thread cleanly. In Spring Boot, this is handled automatically by the starter's OpenFeatureRegistrar.

Spring Boot Starter

flagctl-spring-boot-starter is a zero-boilerplate Spring Boot 3.x auto-configuration. Drop it in and configure three properties — no manual provider setup required.

Installation

xml — pom.xml
<repositories>
  <repository>
    <id>github</id>
    <url>https://maven.pkg.github.com/cloudwalkersinc/flagctl-app</url>
  </repository>
</repositories>

<dependency>
  <groupId>io.flagctl</groupId>
  <artifactId>flagctl-spring-boot-starter</artifactId>
  <version>0.1.0</version>
</dependency>

Configuration

yaml — application.yaml
flagctl:
  base-url: https://acme.flagctl.io    # your workspace URL
  api-key:  fctl_sk_...               # server or personal API key
  environment: production              # environment slug
  poll-interval-seconds: 30          # optional, default 30, min 5
  enabled: true                       # set false to disable in tests

Usage

Inject the OpenFeature Client anywhere in your application:

java
@Service
public class CheckoutService {

    private final Client flags =
        OpenFeatureAPI.getInstance().getClient();

    public OrderResult checkout(User user, Cart cart) {
        MutableContext ctx = new MutableContext(user.getId());
        ctx.add("plan", user.getPlan());

        if (flags.getBooleanValue("checkout-v2", false, ctx)) {
            return checkoutV2(user, cart);
        }
        return checkoutV1(user, cart);
    }
}

Disabling in tests

yaml — src/test/resources/application-test.yaml
flagctl:
  enabled: false

Actuator endpoint

When spring-boot-actuator is on the classpath, /actuator/flagctl reports provider status:

json — GET /actuator/flagctl
{
  "provider": "flagctl",
  "state": "READY",
  "environment": "production",
  "baseUrl": "https://acme.flagctl.io",
  "pollIntervalSeconds": 30
}

To expose it, add flagctl to management.endpoints.web.exposure.include in your config.

CLI reference

The flagctl CLI gives you full flag management from the terminal. It uses the same API as the UI — all operations are reflected in the audit log.

Installation

bash
# Build from source (requires Go 1.22+)
git clone https://github.com/cloudwalkersinc/flagctl-app
cd flagctl-cli
make install   # installs to /usr/local/bin/flagctl

# Or build locally without installing
make build     # outputs to ./bin/flagctl

Authentication

The CLI authenticates using a server or personal API key created in Settings → API Keys. JWT tokens from the browser session are not accepted by the CLI.

bash
# Login — saves config to ~/.flagctl/config.yaml
flagctl auth login \
  --api-key fctl_sk_... \
  --workspace acme

# Verify the stored credentials
flagctl auth whoami

# Log out (removes stored API key)
flagctl auth logout

Context management

The context is the combination of workspace and environment used for all flag commands. You can switch contexts without re-authenticating.

bash
# Set the active context
flagctl context set \
  --workspace acme \
  --environment production

# View the active context
flagctl context get

# Output:
# workspace:   acme
# environment: production

Flag commands

bash
# List all flags in the active environment
flagctl flag list

# Show a specific flag
flagctl flag get checkout-v2

# Enable a flag in the current environment
flagctl flag enable checkout-v2

# Disable a flag
flagctl flag disable checkout-v2

# Set rollout percentage
flagctl flag set-rollout checkout-v2 --pct 50

# Override environment for a single command
flagctl flag list --environment staging

Configuration file

The CLI stores its configuration at ~/.flagctl/config.yaml. You can edit it directly or manage it via flagctl auth and flagctl context commands.

REST API

All flagctl operations are available through the REST API. The base URL is https://<workspace-slug>.flagctl.io.

Authentication

Pass your API key as a Bearer token in the Authorization header:

bash
curl https://acme.flagctl.io/api/v1/flags \
  -H "Authorization: Bearer fctl_sk_..."

Key endpoints

Method Path Description
GET /api/v1/flags List all flags in the workspace
POST /api/v1/flags Create a flag
GET /api/v1/flags/{key} Get a flag by key
PUT /api/v1/flags/{key} Update a flag
DELETE /api/v1/flags/{key} Archive a flag
GET /api/v1/envs List environments
POST /api/v1/envs Create an environment
GET /api/v1/manifest/{envSlug} Fetch the flag manifest for an environment (used by SDKs)
GET /api/v1/audit List audit log entries

Error responses

All error responses return JSON with status, message, and optional detail fields.

json — 404 Not Found
{
  "status": 404,
  "message": "Flag not found: checkout-v3"
}

Manifest API

The Manifest API is the primary data feed for SDK instances. SDKs poll this endpoint to keep their local cache up to date.

http
GET /api/v1/manifest/{envSlug}
Authorization: Bearer fctl_sk_...

Caching

The response includes an ETag header (SHA-256 of the sorted flag list). On subsequent requests, pass the previous ETag as If-None-Match. If the manifest hasn't changed, the API returns 304 Not Modified with no body — minimizing bandwidth at scale.

The Cache-Control header is public, max-age=30, stale-while-revalidate=60.

Response shape

json
{
  "version": 1,
  "generatedAt": "2026-06-17T12:00:00Z",
  "environment": {
    "id": "...",
    "slug": "production",
    "name": "Production",
    "isProduction": true
  },
  "flags": [
    {
      "key": "checkout-v2",
      "type": "boolean",
      "enabled": true,
      "status": "active",
      "defaultValue": "true",
      "rollout": 80,
      "killSwitchArmed": false,
      "updatedAt": "2026-06-15T09:32:11Z"
    }
  ]
}

API keys

API keys authenticate requests from SDKs, the CLI, and direct API calls. They are not the same as your personal login credentials.

Key types

Type Prefix Use for
Server fctl_sk_ Backend services, SDKs, CI/CD pipelines. Has read and write access.
Personal fctl_pk_ CLI use for individual developers. Tied to your user account.

Creating a key

Go to Settings → API Keys → New key. Give it a descriptive name (e.g. production-backend). The full key is shown only once at creation — copy it immediately. All subsequent views show only the last 4 characters.

Revoking a key

Click the key in the API Keys list and select Revoke. Revocation is immediate. SDKs using the revoked key will receive 401 Unauthorized on their next poll cycle.

⚠
Never commit API keys to source control. Use environment variables or a secrets manager (e.g. AWS Secrets Manager, HashiCorp Vault) to inject keys at runtime.

Segments

Segments are named, reusable collections of users defined by attribute rules. Reference a segment in a flag's targeting configuration to serve a specific value to all matching users.

Creating a segment

Go to Segments → New segment. Define one or more rules:

  • Attribute — any attribute passed in the evaluation context (e.g. plan, country, email)
  • Operator — equals, not_equals, contains, starts_with, in
  • Value — the value to match against

Using a segment in a flag

When editing a flag, add a targeting rule that references a segment. Users who match the segment receive the flag's active value, bypassing the percentage rollout.

ℹ
Segment evaluation happens in the SDK against attributes you provide in EvaluationContext. Attributes not passed at evaluation time cannot be matched — make sure your services pass all relevant user attributes.

Audit log

Every mutation to flags, environments, API keys, and members is recorded with full context. The audit log is immutable — entries cannot be deleted or edited.

What's logged

  • Flag created, updated, enabled, disabled, archived
  • Environment created, updated, deleted
  • Kill switch armed, activated
  • API key created, revoked
  • Member invited, removed, role changed

Each entry contains

  • Actor — user email, API key name, or sdk for automated operations
  • Action — e.g. FLAG_ENABLED, KILL_SWITCH_ACTIVATED
  • Resource — flag key, environment slug, etc.
  • Timestamp — UTC ISO-8601
  • Diff — before/after values for flag mutations

Export

Audit entries are accessible via GET /api/v1/audit. Export the full log as JSON or CSV from Settings → Audit Log → Export for SOC 2 or ISO 27001 evidence.

Webhooks Coming soon

Webhooks will let you subscribe to flag change events and receive HTTP POST notifications to your own endpoints — useful for cache invalidation, CI triggers, and Slack alerts.

ℹ
Webhooks are tracked as CLO-21 in our Linear board. Until available, you can poll GET /api/v1/audit for recent changes or use the SDK's polling mechanism.

Planned event types

  • flag.updated — any flag mutation
  • flag.enabled / flag.disabled
  • killswitch.activated
  • environment.updated