Feature flags

Release features gradually, target cohorts, run deterministic rollouts, and read flag variants from every Cohorly SDK.

Feature flags let you control product behavior without shipping a new app version for every change. A flag belongs to one project, is evaluated for one distinct_id, and can return a boolean, a variant key, and an optional JSON payload.

When to use feature flags

Use a flag when you need to:

  • release a feature behind a kill switch;
  • expose a feature to a cohort or a list of users;
  • split traffic between variants for an experiment;
  • configure a small value, such as a label, color, or limit, without a deploy.

Keep the flag decision close to the code that changes behavior. Use Cohorly events and reports to measure the result of the release or experiment.

Core terms

TermMeaning
FlagA project-scoped switch with an immutable key and an active state.
VariantA named value returned by a flag, with a rollout percentage and optional payload.
RuleAn ordered targeting condition based on a cohort, exact distinct IDs, or both.
RolloutThe percentage of matching identities assigned to a flag or variant.
OverrideAn exact distinct ID list used for targeted testing.
Local evaluationServer-side evaluation from definitions cached with a flag secret.
ExposureThe $feature_flag_called event recorded when an SDK reports a flag read.

Create a flag

  1. Open Data > Flags in the dashboard.
  2. Select Create flag.
  3. Enter a human-readable name and a lowercase key. Keys may contain letters, digits, -, and _, and cannot be changed after creation.
  4. Choose whether the flag is active. An inactive flag is disabled for every evaluation.
  5. Add variants when the flag needs more than a simple on/off decision. The rollout percentages of all variants must total 100%.
  6. Add rules from top to bottom. The first matching rule decides the result.
  7. Save the flag and verify it with a test identity before increasing the rollout.

Create cohorts under Audience > Cohorts before using them in a rule. An override is an exact list of distinct_id values, one per line, and is intended for testing individual users. It is not a replacement for cohort targeting.

How evaluation works

For a flag and a distinct_id, Cohorly evaluates the following sequence:

  1. An inactive flag returns enabled: false with reason inactive.
  2. Rules are checked in the order shown in the dashboard.
  3. Every targeting condition configured on a rule must match. A rule with both a cohort and an exact ID list requires both; a rule with neither targets all identities that reach it.
  4. The rule rollout is checked deterministically. If the identity falls outside the rollout, the rule does not match and evaluation continues with the next rule. The same identity remains in the same bucket until the rule or identity changes.
  5. A named variant on the rule wins. Otherwise, the identity is assigned to a variant using the variant rollout percentages.
  6. If no rule matches, the flag returns enabled: false with reason no_match.

The evaluation response includes a reason that is useful when debugging:

ReasonMeaning
inactiveThe flag is turned off.
no_matchNo rule matched the identity.
rule:<index>Rule <index> matched, using zero-based rule order.

An empty rules list is fail-closed: the flag is disabled for everyone. The dashboard's new-flag form starts with a 100% rule so a newly created flag can be tested immediately.

Remote evaluation resolves linked anonymous and identified IDs to the same identity cluster. Local evaluation uses the raw distinct_id supplied to the server SDK. Keep this distinction in mind when comparing client and server results for a user who has logged in.

Read flags in client applications

Client SDKs load the current identity's flags on initialization by default. They cache results by identity, reload after identify() and reset(), and retain the last successful result when a later reload fails. Unknown flags and flags that have not loaded yet are safe off states.

Web

ts
import { init } from "@cohorly/web";
const cohorly = init({
token: "YOUR_PROJECT_TOKEN",
loadFeatureFlags: true,
sendExposureEvents: true,
});
const enabled = cohorly.isFeatureEnabled("checkout-redesign");
const value = cohorly.getFeatureFlag("checkout-redesign");
const payload = cohorly.getFeatureFlagPayload("checkout-redesign");
await cohorly.reloadFeatureFlags();

isFeatureEnabled(key) always returns a boolean. getFeatureFlag(key) returns the variant key when the flag has variants, otherwise a boolean. Before the first successful load it returns false. getFeatureFlagPayload(key) returns the variant payload or null.

Set loadFeatureFlags: false when the application wants to control the first load manually. Set sendExposureEvents: false to disable automatic exposure tracking.

React

The React SDK re-exports the web client and adds hooks that re-render after a successful flag reload:

tsx
import { useFeatureFlag, useFeatureFlagPayload } from "@cohorly/react";
export function Checkout() {
const value = useFeatureFlag("checkout-redesign");
const payload = useFeatureFlagPayload("checkout-redesign");
if (value === undefined) {
return null;
}
const isNewCheckout = value === true || value === "new";
return isNewCheckout ? (
<NewCheckout payload={payload} />
) : (
<LegacyCheckout />
);
}

The hook value is undefined while flags are loading, then becomes a boolean or variant key. The payload hook is undefined while loading and null when the loaded flag has no payload.

Next.js

@cohorly/nextjs re-exports the React API. If you use createCohorlyProxy(), include flags/evaluate in allowedPaths whenever you provide a custom allowlist. The default allowlist already includes this path.

ts
import { createCohorlyProxy } from "@cohorly/nextjs/server";
export const { POST } = createCohorlyProxy({
token: process.env.COHORLY_PROJECT_TOKEN!,
allowedPaths: ["track", "engage", "alias", "flags/evaluate"],
});

See the Next.js SDK guide for the complete proxy setup.

Mobile SDKs

SDKInitialization optionRead and refresh methods
React NativeloadFeatureFlags, sendExposureEventsgetFeatureFlag, isFeatureEnabled, getFeatureFlagPayload, reloadFeatureFlags, onFeatureFlags
iOSloadFeatureFlags, sendExposureEventsgetFeatureFlag, isFeatureEnabled, getFeatureFlagPayload, reloadFeatureFlags, onFeatureFlags
AndroidloadFeatureFlags, sendExposureEventsgetFeatureFlag, isFeatureEnabled, getFeatureFlagPayload, reloadFeatureFlags, onFeatureFlags

Mobile clients persist the identity-scoped cache in their platform storage. The first load is asynchronous. Render a safe loading state until the SDK has returned a value.

Read the platform-specific React Native, iOS, and Android references for initialization and type details.

Read flags in server applications

Server SDKs evaluate flags remotely by default. Add a flag secret when you want local evaluation for flags marked localEvaluable. The SDK polls the definitions endpoint and evaluates local rules without sending a request for each read. Flags that depend on cohorts, or are not present in the local definitions, fall back to remote evaluation.

Use local evaluation for low-latency request paths. Use remote evaluation when you need the server to make the decision from the latest state on every read.

Node.js

ts
import Cohorly from "@cohorly/node";
const cohorly = Cohorly.init(process.env.COHORLY_PROJECT_TOKEN!, {
flagSecret: process.env.COHORLY_FLAG_SECRET,
flagPollIntervalMs: 30000,
});
await cohorly.flagDefinitionsReady;
const enabled = await cohorly.flags.isFeatureEnabled(
"checkout-redesign",
"user-123",
);
const payload = await cohorly.flags.getFeatureFlagPayload(
"checkout-redesign",
"user-123",
);

Single server reads do not create exposure events unless the call opts in with { sendExposureEvent: true }. getAllFlags() never creates exposures. Stop the definitions poller during process shutdown with cohorly.shutdown().

NestJS

Configure the same options in CohorlyModule.forRoot() and inject CohorlyService:

ts
CohorlyModule.forRoot({
token: process.env.COHORLY_PROJECT_TOKEN!,
flagSecret: process.env.COHORLY_FLAG_SECRET,
flagPollIntervalMs: 30000,
});

The service exposes isFeatureEnabled, getFeatureFlag, getFeatureFlagPayload, and getAllFlags. The raw .flags API is also available when the lower-level client is needed.

Python

python
from cohorly import Cohorly
client = Cohorly(
"YOUR_PROJECT_TOKEN",
flag_secret="YOUR_FLAG_SECRET",
flag_poll_interval_seconds=30.0,
)
enabled = client.is_feature_enabled("checkout-redesign", "user-123")
value = client.get_feature_flag("checkout-redesign", "user-123")
payload = client.get_feature_flag_payload("checkout-redesign", "user-123")

Python refreshes definitions lazily when a flag is read. It does not create a background polling thread. Set send_exposure_event=True for a single read; get_all_flags() never creates exposures.

PHP

php
use Cohorly\Cohorly;
$cohorly = Cohorly::getInstance('YOUR_PROJECT_TOKEN', [
'flag_secret' => getenv('COHORLY_FLAG_SECRET'),
'flag_poll_interval' => 30,
]);
$enabled = $cohorly->flags->isFeatureEnabled('checkout-redesign', 'user-123');
$payload = $cohorly->flags->getFeatureFlagPayload('checkout-redesign', 'user-123');

PHP flag reads fail safe: a failed evaluation returns false, null, or an empty list and writes a debug log entry instead of throwing. Pass true as the third argument to a single read to send an exposure event.

Go

go
package main
import (
"context"
"os"
cohorly "github.com/cohorly-io/cohorly-go"
)
func readFlag(ctx context.Context) (bool, error) {
client := cohorly.NewClient(
os.Getenv("COHORLY_PROJECT_TOKEN"),
cohorly.WithFlagSecret(os.Getenv("COHORLY_FLAG_SECRET")),
)
defer client.Close()
return client.IsFeatureEnabled(ctx, "checkout-redesign", "user-123")
}

Use WithExposureEvent() on a single read when the decision should be measured. GetAllFlags() never creates exposures. Close() stops the local definitions poller.

Exposure events

Client SDKs send an exposure event the first time an identity reads a given flag/value pair in an identity session. The event is deduplicated, so repeated renders do not inflate exposure counts. Server SDKs require per-call opt-in.

The event name and properties are stable across SDKs:

json
{
"event": "$feature_flag_called",
"properties": {
"distinct_id": "user-123",
"$feature_flag": "checkout-redesign",
"$feature_flag_response": "variant-b"
}
}

$feature_flag_response is the served variant key or a boolean for a simple flag. getAllFlags() is intentionally not an exposure API. If you need to measure a flag decision on the server, use one of the single-read methods with exposure tracking enabled.

Flag secrets and security

A project token authenticates ingestion and remote evaluation. A flag secret is different: it only authorizes GET /flags/local-evaluation for server-side definition polling.

  • Keep the flag secret in a server-side secret manager.
  • Never include it in browser or mobile bundles.
  • Only an organization owner or superadmin can generate, rotate, or revoke it.
  • The secret is shown only when it is created or rotated.
  • Rotating a secret invalidates the previous secret immediately.
  • Revoking a secret rejects future definition refreshes. Running server SDKs retain their last successful definitions by design; restart them or remove the secret from their configuration to force remote evaluation immediately.

Release checklist

  1. Choose a stable key that describes behavior, not a release date.
  2. Start inactive or at a small rollout.
  3. Test with an exact ID override.
  4. Confirm the flag result and optional payload in the target client or server.
  5. Increase the rollout gradually and watch the related Cohorly report.
  6. Keep the flag active only while it controls a real decision.
  7. Remove obsolete flags and their code after the rollout is complete.

Troubleshooting

The flag always returns false

Check that the flag is active, has at least one rule, and that the identity matches the intended cohort or exact ID. An empty rules list and no_match are both disabled outcomes.

A variant is not returned

Check that the variant rollout percentages total 100%, that the matching rule names an existing variant when it specifies one, and that the caller uses getFeatureFlag() rather than only isFeatureEnabled().

A client shows an old value

Call reloadFeatureFlags() after a dashboard change. Client SDKs retain the last successful cache when a request fails, which prevents a transient network error from unexpectedly disabling a live feature.

Local evaluation is not taking effect

Confirm that the flag secret belongs to the same project, has not been rotated or revoked, and that the flag is marked localEvaluable. Cohort rules and unknown flags intentionally fall back to remote evaluation.

For request and response shapes, authentication, limits, and error codes, see the Feature Flags API reference.

PreviousGo
NextIngestion