Skip to main content

Manage feature flags as code with API automation, CI/CD pipelines, and version-controlled configurations

Infrastructure as Code

Manage your feature flags as code alongside your application. Use Flaggr's API to automate flag creation, promote configurations between environments, validate changes in CI/CD, and maintain flag definitions in version control.

Why IaC for Feature Flags?

BenefitHow
ReproducibilityFlag configurations are version-controlled and can be recreated from scratch
AuditabilityChanges go through pull requests with code review
ConsistencySame flags across environments, applied automatically
RollbackGit revert restores previous flag state
TestingValidate flag configurations before applying them

API-Driven Automation

All flag operations are available through the REST API. Use JWT tokens for programmatic access with fine-grained permissions.

Creating a JWT Token for CI/CD

curl -X POST /api/tokens \
  -H "Authorization: Bearer flg_admin_token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI/CD Pipeline",
    "tokenType": "jwt",
    "permissions": { "read": true, "write": true, "delete": false },
    "scopes": ["service:web-app:read", "service:web-app:write"],
    "expiresAt": "2027-01-01T00:00:00Z"
  }'
Info

Use scoped JWT tokens for CI/CD — limit access to specific services and avoid granting delete permissions. Rotate tokens regularly.

Flag Definitions in Version Control

Store your flag definitions as JSON files in your repository:

repo/
├── flags/
│   ├── development.json
│   ├── staging.json
│   └── production.json
└── .github/
    └── workflows/
        └── sync-flags.yml

Flag Definition File

{
  "flags": [
    {
      "key": "checkout-v2",
      "name": "Checkout V2",
      "description": "New streamlined checkout flow",
      "type": "boolean",
      "enabled": false,
      "defaultValue": false,
      "serviceId": "web-app",
      "environment": "production",
      "tags": ["checkout", "q1-2026"],
      "targeting": [
        {
          "id": "beta-users",
          "conditions": [
            { "property": "plan", "operator": "equals", "value": "enterprise" }
          ],
          "rolloutPercentage": 100
        }
      ]
    },
    {
      "key": "dark-mode",
      "name": "Dark Mode",
      "description": "Toggle dark mode support",
      "type": "boolean",
      "enabled": true,
      "defaultValue": false,
      "serviceId": "web-app",
      "environment": "production",
      "tags": ["ui", "permanent"]
    }
  ]
}

CI/CD Pipeline Examples

GitHub Actions: Validate and Sync Flags

name: Sync Feature Flags
on:
  push:
    branches: [main]
    paths:
      - 'flags/**'
  pull_request:
    paths:
      - 'flags/**'
 
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Validate flag definitions
        run: |
          for file in flags/*.json; do
            echo "Validating $file..."
            # Validate JSON syntax
            jq empty "$file" || exit 1
 
            # Validate required fields
            jq -e '.flags | length > 0' "$file" > /dev/null || {
              echo "Error: $file has no flags"
              exit 1
            }
 
            # Validate flag keys match naming convention
            jq -e '.flags[] | select(.key | test("^[a-zA-Z][a-zA-Z0-9_-]*$") | not) | .key' "$file" \
              | while read -r key; do
                echo "Error: Invalid flag key: $key"
                exit 1
              done
          done
          echo "All flag definitions valid"
 
      - name: Dry run import (PR only)
        if: github.event_name == 'pull_request'
        env:
          FLAGGR_API_URL: ${{ vars.FLAGGR_API_URL }}
          FLAGGR_TOKEN: ${{ secrets.FLAGGR_CI_TOKEN }}
        run: |
          for file in flags/*.json; do
            echo "Dry run: $file"
            RESULT=$(curl -s -X POST "${FLAGGR_API_URL}/api/flags/import" \
              -H "Authorization: Bearer ${FLAGGR_TOKEN}" \
              -H "Content-Type: application/json" \
              -d "$(jq '{flags: .flags, conflictResolution: "skip", dryRun: true}' "$file")")
 
            echo "$RESULT" | jq .
            ERRORS=$(echo "$RESULT" | jq '.errors | length')
            if [ "$ERRORS" -gt 0 ]; then
              echo "::error::Import validation failed with $ERRORS errors"
              exit 1
            fi
          done
 
  sync:
    needs: validate
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Sync flags to Flaggr
        env:
          FLAGGR_API_URL: ${{ vars.FLAGGR_API_URL }}
          FLAGGR_TOKEN: ${{ secrets.FLAGGR_CI_TOKEN }}
        run: |
          for file in flags/*.json; do
            echo "Syncing $file..."
            RESULT=$(curl -s -X POST "${FLAGGR_API_URL}/api/flags/import" \
              -H "Authorization: Bearer ${FLAGGR_TOKEN}" \
              -H "Content-Type: application/json" \
              -d "$(jq '{flags: .flags, conflictResolution: "overwrite", dryRun: false}' "$file")")
 
            echo "$RESULT" | jq .
            SUCCESS=$(echo "$RESULT" | jq '.success')
            if [ "$SUCCESS" != "true" ]; then
              echo "::error::Flag sync failed"
              exit 1
            fi
          done
          echo "All flags synced successfully"

Generic Bash Script

A reusable script for any CI/CD system:

#!/bin/bash
# sync-flags.sh — Sync flag definitions to Flaggr
set -euo pipefail
 
FLAGGR_API_URL="${FLAGGR_API_URL:?Missing FLAGGR_API_URL}"
FLAGGR_TOKEN="${FLAGGR_TOKEN:?Missing FLAGGR_TOKEN}"
FLAGS_DIR="${FLAGS_DIR:-./flags}"
DRY_RUN="${DRY_RUN:-false}"
CONFLICT="${CONFLICT:-skip}"
 
for file in "$FLAGS_DIR"/*.json; do
  [ -f "$file" ] || continue
  echo "Processing: $file"
 
  RESULT=$(curl -sf -X POST "${FLAGGR_API_URL}/api/flags/import" \
    -H "Authorization: Bearer ${FLAGGR_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "$(jq --arg cr "$CONFLICT" --argjson dr "$DRY_RUN" \
      '{flags: .flags, conflictResolution: $cr, dryRun: $dr}' "$file")")
 
  CREATED=$(echo "$RESULT" | jq '.created')
  UPDATED=$(echo "$RESULT" | jq '.updated')
  SKIPPED=$(echo "$RESULT" | jq '.skipped')
  ERRORS=$(echo "$RESULT" | jq '.errors | length')
 
  echo "  Created: $CREATED, Updated: $UPDATED, Skipped: $SKIPPED, Errors: $ERRORS"
 
  if [ "$ERRORS" -gt 0 ]; then
    echo "$RESULT" | jq '.errors[]'
    exit 1
  fi
done
 
echo "Done"

Usage:

# Dry run
DRY_RUN=true CONFLICT=skip ./sync-flags.sh
 
# Sync with overwrite
DRY_RUN=false CONFLICT=overwrite ./sync-flags.sh

Environment Promotion

Automate flag promotion from staging to production:

#!/bin/bash
# promote-flags.sh — Promote flags from staging to production
set -euo pipefail
 
FLAGGR_API_URL="${FLAGGR_API_URL:?Missing FLAGGR_API_URL}"
FLAGGR_TOKEN="${FLAGGR_TOKEN:?Missing FLAGGR_TOKEN}"
PROJECT_ID="${PROJECT_ID:?Missing PROJECT_ID}"
 
# 1. Export from staging
echo "Exporting staging flags..."
curl -sf "${FLAGGR_API_URL}/api/flags/export?projectId=${PROJECT_ID}&environment=staging" \
  -H "Authorization: Bearer ${FLAGGR_TOKEN}" \
  -o staging-export.json
 
# 2. Transform for production (disabled by default for safety)
echo "Transforming for production..."
jq '.flags |= map(.environment = "production" | .enabled = false)' \
  staging-export.json > production-import.json
 
FLAG_COUNT=$(jq '.flags | length' production-import.json)
echo "  $FLAG_COUNT flags to promote"
 
# 3. Dry run
echo "Running dry run..."
RESULT=$(curl -sf -X POST "${FLAGGR_API_URL}/api/flags/import" \
  -H "Authorization: Bearer ${FLAGGR_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "$(jq '{flags: .flags, conflictResolution: "skip", dryRun: true}' production-import.json)")
 
echo "$RESULT" | jq '{created, updated, skipped, errors: (.errors | length)}'
 
# 4. Confirm and apply
read -p "Apply to production? (yes/no): " CONFIRM
if [ "$CONFIRM" = "yes" ]; then
  curl -sf -X POST "${FLAGGR_API_URL}/api/flags/import" \
    -H "Authorization: Bearer ${FLAGGR_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "$(jq '{flags: .flags, conflictResolution: "skip", dryRun: false}' production-import.json)" | jq .
  echo "Promotion complete"
else
  echo "Cancelled"
fi
 
# Cleanup
rm -f staging-export.json production-import.json

PR Validation with Dry Run

Add flag validation to your PR checks to catch issues before merge:

# .github/workflows/validate-flags.yml
name: Validate Flags
on:
  pull_request:
    paths: ['flags/**']
 
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Validate and dry-run import
        env:
          FLAGGR_API_URL: ${{ vars.FLAGGR_API_URL }}
          FLAGGR_TOKEN: ${{ secrets.FLAGGR_CI_TOKEN }}
        run: |
          # Find changed flag files
          CHANGED=$(git diff --name-only origin/main -- 'flags/*.json')
          if [ -z "$CHANGED" ]; then
            echo "No flag files changed"
            exit 0
          fi
 
          for file in $CHANGED; do
            echo "::group::Validating $file"
            RESULT=$(curl -sf -X POST "${FLAGGR_API_URL}/api/flags/import" \
              -H "Authorization: Bearer ${FLAGGR_TOKEN}" \
              -H "Content-Type: application/json" \
              -d "$(jq '{flags: .flags, conflictResolution: "skip", dryRun: true}' "$file")")
 
            echo "$RESULT" | jq .
            ERRORS=$(echo "$RESULT" | jq '.errors | length')
            if [ "$ERRORS" -gt 0 ]; then
              echo "::error::Validation failed for $file"
              exit 1
            fi
            echo "::endgroup::"
          done
 
      - name: Comment on PR
        if: success()
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: 'Flag validation passed. All flag definitions are valid and ready to sync.'
            })

Token Management for CI/CD

Best Practices

PracticeWhy
Use JWT tokens with scopesLimit blast radius if token is compromised
Set expiration datesForce periodic rotation
Store as CI/CD secretsNever commit tokens to code
Separate read and write tokensValidation needs read-only; sync needs write
Rotate every 90 daysReduce risk from leaked credentials

Creating Purpose-Specific Tokens

# Read-only token for PR validation
curl -X POST /api/tokens \
  -H "Authorization: Bearer flg_admin_token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI Validation (read-only)",
    "tokenType": "jwt",
    "permissions": { "read": true, "write": false, "delete": false }
  }'
 
# Write token for flag sync (main branch only)
curl -X POST /api/tokens \
  -H "Authorization: Bearer flg_admin_token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI Flag Sync (write)",
    "tokenType": "jwt",
    "permissions": { "read": true, "write": true, "delete": false }
  }'

Terraform Provider

Info

A Terraform provider for Flaggr is planned. In the meantime, use the REST API with the local-exec provisioner or the scripts above.

# Planned — not yet available
resource "flaggr_flag" "checkout_v2" {
  key          = "checkout-v2"
  name         = "Checkout V2"
  type         = "boolean"
  enabled      = false
  default_value = false
  service_id   = flaggr_service.web_app.id
  environment  = "production"
}