Skip to main content

End-to-end guide to creating feature flags — choosing types, naming, setting defaults, and environment scoping

Creating Flags

This guide walks through everything you need to know to create feature flags in Flaggr. By the end you'll understand flag types, naming conventions, default values, environment scoping, and how creation interacts with the approval workflow.

Prerequisites

Before creating a flag you need:

  1. A project — the top-level tenant that groups services
  2. A service — the application or microservice the flag belongs to
  3. At least write permission on the project

If you're starting from scratch, see the Quick Start to create a project and service first.

Creating a Flag via the Dashboard

  1. Navigate to your project in the console
  2. Select the service the flag belongs to
  3. Click Create Flag in the flag dashboard toolbar
  4. Fill in the form fields described below
  5. Click Create

The flag is created in the currently selected environment (shown in the environment selector in the header). It starts disabled by default.

Creating a Flag via the API

curl -X POST https://flaggr.dev/api/flags \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "new-checkout-flow",
    "name": "New Checkout Flow",
    "type": "boolean",
    "defaultValue": false,
    "enabled": false,
    "serviceId": "svc-abc123",
    "environment": "development",
    "description": "Redesigned checkout experience with single-page layout"
  }'

Response: 201 Created with the full flag object.

Flag Fields

Required Fields

FieldTypeDescription
keystringUnique identifier. Must start with a letter, contain only a-z, A-Z, 0-9, -, _. Max 100 characters.
namestringHuman-readable display name. Max 200 characters.
typeenumOne of boolean, string, number, object. Cannot be changed after creation.
defaultValuevariesMust match the declared type. This is the value returned when no targeting rule matches.
serviceIdstringThe service this flag belongs to.

Optional Fields

FieldTypeDefaultDescription
descriptionstring""Explain what the flag controls. Max 1000 characters.
environmentstring"development"Which environment to create the flag in.
enabledbooleanfalseWhether the flag is active. Disabled flags always return defaultValue.
isPublicbooleanfalseIf true, the flag can be evaluated without authentication.
tagsstring[][]Labels for organization. Max 20 tags, each max 50 characters.
variantsarray[]Multi-variant configuration. See Variants.
targetingarray[]Targeting rules. Usually added after creation via the UI.
overridesarray[]Per-user value overrides. Usually added after creation.

Choosing a Flag Type

The flag type determines what values the flag can return. Choose based on the decision your code needs to make:

TypeUse WhenDefault Value ExampleCode Pattern
booleanOn/off feature togglefalseif (flag) { showFeature() }
stringChoosing between named options"control"switch (flag) { case "v2": ... }
numberTuning a numeric parameter10const limit = flag
objectComplex configuration{"theme": "light"}const config = flag

Boolean flags

The most common type. Boolean flags have special toggle behavior — when you toggle a boolean flag in the UI, both enabled and defaultValue are updated together. This means:

  • Toggle onenabled: true, defaultValue: true
  • Toggle offenabled: false, defaultValue: false

For non-boolean flags, toggling only changes the enabled field.

String flags

Use string flags when you need to switch between named alternatives. Common patterns:

  • UI variants: "control", "treatment_a", "treatment_b"
  • Algorithm selection: "v1", "v2", "v3"
  • Theme selection: "light", "dark", "system"

String flags work well with the variants system for A/B testing.

Number flags

Use number flags to tune parameters without code changes:

  • Rate limits: 100 requests per minute
  • Timeouts: 5000 milliseconds
  • Batch sizes: 50 items per page
  • Rollout thresholds: 25 percent

Object flags

Use object flags when a single feature requires multiple configuration values:

{
  "maxRetries": 3,
  "timeoutMs": 5000,
  "enableFallback": true,
  "fallbackUrl": "https://backup.example.com"
}

Object values must be valid JSON. They are stored and returned as-is.

Naming Conventions

Good flag keys are self-documenting. Follow these patterns:

Do

PatternExampleWhy
Feature namenew-checkout-flowDescribes what it controls
Component + behaviorsidebar-collapsed-defaultSpecific and scannable
Experiment namepricing-page-v2Ties to a known initiative

Don't

PatternExampleWhy Not
Generictest, flag-1, new-thingImpossible to understand later
Too longenable-the-new-redesigned-checkout-flow-with-single-page-layoutHard to type in code
Dates or ticketsJIRA-1234, 2026-03-featureBreaks when the ticket moves or the date passes
Negativesdisable-old-flowDouble negatives when checking if (!flag)

Key format rules

  • Start with a letter (a-z or A-Z)
  • Only letters, numbers, hyphens, and underscores
  • Maximum 100 characters
  • Case-sensitive: my-flag and My-Flag are different flags

Environment Scoping

Every flag is scoped to a specific environment. The same key can exist independently in development, staging, and production — they are completely separate records with separate values, targeting rules, and enabled states.

How environments work

Project: "my-app"
  └─ Service: "web-frontend"
       ├─ development / new-checkout-flow  →  enabled: true,  defaultValue: true
       ├─ staging     / new-checkout-flow  →  enabled: true,  defaultValue: false
       └─ production  / new-checkout-flow  →  enabled: false, defaultValue: false

Creating a flag in development does not automatically create it in staging or production. Each environment must be populated separately, either by:

  1. Creating manually in each environment
  2. Promoting from one environment to another via the API:
POST /api/projects/{id}/environments/development/promote
{
  "targetEnvironment": "staging",
  "serviceId": "svc-abc123",
  "flagKeys": ["new-checkout-flow"]
}

Default environments

Every project starts with three environments:

EnvironmentColorTypical Use
developmentBlueLocal development, feature work
stagingYellowPre-production testing, QA
productionRedLive traffic

You can create custom environments (e.g., canary, preview) via the project settings.

Protected environments

Environments can be configured with requireApproval: true and protectedEnvironment: true. When this is set, any flag creation, update, or toggle in that environment returns HTTP 202 Accepted with a change request instead of applying the change immediately. The change must be approved before it takes effect.

Variants

Variants let a single flag return different values for different users, making them essential for A/B testing and multi-armed experiments.

Defining variants at creation

curl -X POST https://flaggr.dev/api/flags \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "pricing-page-layout",
    "name": "Pricing Page Layout",
    "type": "string",
    "defaultValue": "control",
    "serviceId": "svc-abc123",
    "environment": "production",
    "variants": [
      { "name": "control", "value": "grid", "weight": 50 },
      { "name": "treatment", "value": "comparison-table", "weight": 50 }
    ]
  }'

How variant assignment works

When a flag has variants and a targetingKey is provided in the evaluation context:

  1. Flaggr computes a hash: hash(flagKey + targetingKey) % 100
  2. The hash falls into one variant's weight bucket
  3. The same user always gets the same variant (sticky assignment)

Weights are relative — they don't need to sum to 100, but it's a best practice to make them sum exactly.

Variants vs targeting rules

  • Variants assign users randomly by weight — use for experiments
  • Targeting rules assign users by attributes — use for conditional rollouts

You can combine both: a targeting rule can return a specific variant name via the variant field.

What Happens After Creation

When a flag is created, Flaggr:

  1. Validates the request against the schema (type checking, key format, value/type consistency)
  2. Checks for duplicates — the same key + serviceId + environment cannot exist twice (HTTP 409)
  3. Stores the flag in Firestore (production) or the local JSON file (development)
  4. Saves a version snapshot — the initial state is recorded for version history and rollback
  5. Logs an audit event — who created the flag, when, and with what configuration
  6. Invalidates caches — the service's flag list cache is cleared so the new flag appears immediately
  7. Publishes a real-time event — SSE clients subscribed to the service receive a CREATED event

The flag starts disabled — it will return the defaultValue with reason DISABLED for all evaluations until you enable it.

Common Patterns

Feature gate (boolean)

The simplest pattern — hide a feature behind a boolean flag:

const showNewUI = await client.evaluateFlag("new-checkout-flow", {
  targetingKey: user.id
});
 
if (showNewUI) {
  renderNewCheckout();
} else {
  renderOldCheckout();
}

Configuration flag (object)

Ship tunable configuration without deploys:

const config = await client.evaluateFlag("search-config", {
  targetingKey: "global"
});
 
const results = await search(query, {
  maxResults: config.maxResults,
  timeout: config.timeoutMs,
  algorithm: config.algorithm
});

Experiment flag (string with variants)

Run an A/B test with variant assignment:

const variant = await client.evaluateFlag("onboarding-flow", {
  targetingKey: user.id
});
 
switch (variant) {
  case "wizard":    return <OnboardingWizard />;
  case "checklist": return <OnboardingChecklist />;
  default:          return <OnboardingClassic />;
}

Troubleshooting

"Flag with key already exists" (409)

The combination of key + serviceId + environment must be unique. Check if the flag already exists in the target environment. If you need the same flag in another environment, the existing one is a separate record — you can create it there independently.

"Type cannot be changed after creation"

Flag type is immutable. If you need to change a flag's type, create a new flag with the correct type and migrate your code to use the new key.

Default value doesn't match type

The server validates that defaultValue matches the declared type. For example, a number flag with defaultValue: "ten" will fail validation. For object flags, the value must be valid JSON.

Flag created but not visible

Check the environment selector in the UI header — the flag was created in whichever environment was selected at the time. Switch environments to find it.