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?
| Benefit | How |
|---|---|
| Reproducibility | Flag configurations are version-controlled and can be recreated from scratch |
| Auditability | Changes go through pull requests with code review |
| Consistency | Same flags across environments, applied automatically |
| Rollback | Git revert restores previous flag state |
| Testing | Validate 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"
}'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.shEnvironment 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.jsonPR 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
| Practice | Why |
|---|---|
| Use JWT tokens with scopes | Limit blast radius if token is compromised |
| Set expiration dates | Force periodic rotation |
| Store as CI/CD secrets | Never commit tokens to code |
| Separate read and write tokens | Validation needs read-only; sync needs write |
| Rotate every 90 days | Reduce 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
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"
}Related
- Import/Export — Export and import flag configurations
- API Tokens — Token types, scopes, and management
- Flag Lifecycle — Planning and retiring flags
- Versioning — Track changes and roll back
- REST API Reference — Complete endpoint reference