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.
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).
Create an environment
Go to Environments and create one — for example production. Each environment has a slug used in API calls and SDK configuration.
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.
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_.
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.
Core concepts
flagctl is built around a small set of primitives that compose together.
Flag
A named boolean, string, integer, or double value that can be toggled per environment.
Environment
An isolated deployment context (e.g. production, staging). Each flag has independent state per environment.
Workspace
Tenant boundary — maps to a subdomain. Flags, environments, and API keys are scoped to a workspace.
Targeting
Serve different values to different users via percentage rollouts and segment rules.
Kill switch
Arm a flag to give your team an emergency shut-off, with ZSP and a typed confirmation.
Audit log
Every flag change — actor, timestamp, diff — recorded immutably.
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 |
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
- If the flag key is not found in the manifest → return default value with reason
ERROR / FLAG_NOT_FOUND - If
killSwitchArmedistrueand the flag is not enabled → return default value with reasonDISABLED - If the flag is not enabled → return default value with reason
DISABLED - If
rollout < 100and a targeting key (user ID) is provided → bucket the user deterministically. Users outside the bucket receive the default value with reasonDEFAULT - Otherwise → return
flag.defaultValuewith reasonDEFAULT
Evaluation reasons
| Reason | Meaning |
|---|---|
DEFAULT | Flag is enabled; value comes from the flag's configured default value. |
DISABLED | Flag is disabled or kill switch has been activated. |
ERROR | Evaluation 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-v2but not fornew-pricing - Changing the rollout percentage from 20% to 30% adds new users but does not reassign existing ones
// 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);
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.
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:
<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:
<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:
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
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
<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
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:
@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
flagctl: enabled: false
Actuator endpoint
When spring-boot-actuator is on the classpath, /actuator/flagctl reports provider status:
{
"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
# 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.
# 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.
# 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
# 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:
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.
{
"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.
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
{
"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.
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.
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
sdkfor 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.
GET /api/v1/audit for recent changes or use the SDK's polling mechanism.Planned event types
flag.updated— any flag mutationflag.enabled/flag.disabledkillswitch.activatedenvironment.updated