Feature Flags
Category: Operations ยท ๐ฆ 1 install
This page is generated from the Air Pipe marketplace. Browse it live to install into your organization.
A Postgres-backed feature flag service. Create, toggle, and check flags from any service or deployment pipeline without touching application code.
What's includedโ
| File | Purpose |
|---|---|
config.yml | AirPipe config with docs: true |
schema.sql | Feature flags table + seed data |
Endpointsโ
| Method | Path | Description |
|---|---|---|
GET | /flags | List all flags |
POST | /flags/check | Is a flag enabled? |
POST | /flags/create | Create a flag |
POST | /flags/toggle | Flip enabled state |
POST | /flags/set | Explicitly set enabled/disabled |
POST | /flags/delete | Remove a flag |
Setupโ
1. Run the schemaโ
psql $DATABASE_URL -f schema.sql
The schema seeds three example flags (new_dashboard, beta_api_v2, dark_mode).
2. Set managed variableโ
| Name | Value |
|---|---|
DATABASE_URL | your Postgres connection string |
Testingโ
BASE=https://your-airpipe-host
# List all flags
curl $BASE/flags
# Check a specific flag
curl -X POST $BASE/flags/check \
-H "Content-Type: application/json" \
-d '{"name": "new_dashboard"}'
# โ {"name":"new_dashboard","enabled":false}
# Enable it
curl -X POST $BASE/flags/toggle \
-H "Content-Type: application/json" \
-d '{"name": "new_dashboard"}'
# โ {"name":"new_dashboard","enabled":true,...}
# Check it again
curl -X POST $BASE/flags/check \
-H "Content-Type: application/json" \
-d '{"name": "new_dashboard"}'
# โ {"name":"new_dashboard","enabled":true}
Checking flags from your applicationโ
async function isEnabled(flagName) {
const res = await fetch('https://your-airpipe-host/flags/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: flagName })
});
const { enabled } = await res.json();
return enabled;
}
if (await isEnabled('new_dashboard')) {
// show new UI
}
Protecting these endpointsโ
Flag management endpoints should not be public. Add an API key check as the first action in each interface:
- name: ValidateApiKey
input: a|headers|
hide_data_on_success: true
assert:
http_code_on_error: 403
tests:
- value: x-api-key
is_equal_to: a|ap_var::FLAGS_API_KEY|
The read-only /flags/check endpoint can remain public if flags are not sensitive.
Notesโ
- Flag names are validated against
^[a-z0-9_]+$on creation to keep them consistent and URL-safe. flags/toggleflips the current state.flags/setis for when you need an explicit value (e.g. from a CI pipeline:enabled: falseon deploy,enabled: trueafter smoke tests pass).- All updates set
updated_at = NOW()so you have a change history you can query.
Configurationโ
config.ymlโ
name: FeatureFlags
description: Postgres-backed feature flag API. Create, toggle, and check flags from any service.
docs: true
global:
databases:
main:
driver: postgres
conn_string: "a|ap_var::DATABASE_URL|"
interfaces:
# GET /flags
# List all flags with their current state.
flags:
output: http
summary: List flags
description: Returns all feature flags and their current enabled state.
tags: [flags]
response_example:
- id: 1
name: new_dashboard
enabled: false
description: Redesigned dashboard UI
updated_at: "2024-01-01T12:00:00Z"
actions:
- name: ListFlags
database: main
query: |
SELECT id, name, enabled, description, created_at, updated_at
FROM feature_flags
ORDER BY name;
# POST /flags/check
# Check whether a specific flag is enabled. Returns a simple boolean
# response suitable for use in client-side code.
# Body: { "name": "new_dashboard" }
flags/check:
output: http
method: POST
summary: Check flag
description: Returns whether the named flag is currently enabled.
tags: [flags]
request_example:
name: new_dashboard
response_example:
name: new_dashboard
enabled: false
actions:
- name: ValidateBody
input: a|body|
hide_data_on_success: true
assert:
http_code_on_error: 400
tests:
- value: name
is_not_null: true
description: "Flag name"
- name: CheckFlag
run_when_succeeded:
actions: [ValidateBody]
http_code_on_error: 400
database: main
query: |
SELECT name, enabled
FROM feature_flags
WHERE name = $1;
params:
- a|ValidateBody::name|
assert:
http_code_on_error: 404
error_message: "Flag not found"
tests:
- value: count()
is_equal_to: 1
post_transforms:
- extract_value: "[0]"
# POST /flags/create
# Create a new flag. Flags start disabled by default.
# Body: { "name": "my_new_feature", "description": "..." }
flags/create:
output: http
method: POST
summary: Create flag
description: Create a new feature flag. Flags are disabled by default.
tags: [flags]
request_example:
name: my_new_feature
description: "Description of what this flag controls"
actions:
- name: ValidateBody
input: a|body|
hide_data_on_success: true
assert:
http_code_on_error: 400
tests:
- value: name
is_not_null: true
is_not_empty: true
regex: "^[a-z0-9_]+$"
description: "Flag name โ lowercase letters, numbers, underscores only"
- name: CreateFlag
run_when_succeeded:
actions: [ValidateBody]
http_code_on_error: 400
database: main
# ON CONFLICT keeps a duplicate name from raising a raw 500 โ it returns
# zero rows, and the assertion below turns that into a clean 409.
query: |
INSERT INTO feature_flags (name, description)
VALUES ($1, $2)
ON CONFLICT (name) DO NOTHING
RETURNING id, name, enabled, description, created_at;
params:
- a|ValidateBody::name|
- a|ValidateBody::description|
assert:
http_code_on_error: 409
error_message: "A flag with this name already exists"
tests:
- value: count()
is_equal_to: 1
post_transforms:
- extract_value: "[0]"
# POST /flags/toggle
# Flip a flag's enabled state.
# Body: { "name": "new_dashboard" }
flags/toggle:
output: http
method: POST
summary: Toggle flag
description: Flip a flag between enabled and disabled.
tags: [flags]
request_example:
name: new_dashboard
response_example:
name: new_dashboard
enabled: true
updated_at: "2024-01-01T12:00:00Z"
actions:
- name: ValidateBody
input: a|body|
hide_data_on_success: true
assert:
http_code_on_error: 400
tests:
- value: name
is_not_null: true
description: "Flag name"
- name: ToggleFlag
run_when_succeeded:
actions: [ValidateBody]
http_code_on_error: 400
database: main
query: |
UPDATE feature_flags
SET enabled = NOT enabled, updated_at = NOW()
WHERE name = $1
RETURNING name, enabled, updated_at;
params:
- a|ValidateBody::name|
assert:
http_code_on_error: 404
error_message: "Flag not found"
tests:
- value: count()
is_equal_to: 1
post_transforms:
- extract_value: "[0]"
# POST /flags/set
# Explicitly set a flag's state.
# Body: { "name": "new_dashboard", "enabled": true }
flags/set:
output: http
method: POST
summary: Set flag state
description: Explicitly enable or disable a flag.
tags: [flags]
request_example:
name: new_dashboard
enabled: true
actions:
- name: ValidateBody
input: a|body|
hide_data_on_success: true
assert:
http_code_on_error: 400
tests:
- value: name
is_not_null: true
description: "Flag name"
- value: enabled
is_not_null: true
description: "true or false"
- name: SetFlag
run_when_succeeded:
actions: [ValidateBody]
http_code_on_error: 400
database: main
query: |
UPDATE feature_flags
SET enabled = $2, updated_at = NOW()
WHERE name = $1
RETURNING name, enabled, updated_at;
params:
- a|ValidateBody::name|
- a|ValidateBody::enabled|
assert:
http_code_on_error: 404
error_message: "Flag not found"
tests:
- value: count()
is_equal_to: 1
post_transforms:
- extract_value: "[0]"
# POST /flags/delete
# Remove a flag permanently.
# Body: { "name": "old_feature" }
flags/delete:
output: http
method: POST
summary: Delete flag
description: Permanently remove a feature flag.
tags: [flags]
request_example:
name: old_feature
actions:
- name: ValidateBody
input: a|body|
hide_data_on_success: true
assert:
http_code_on_error: 400
tests:
- value: name
is_not_null: true
description: "Flag name"
- name: DeleteFlag
run_when_succeeded:
actions: [ValidateBody]
http_code_on_error: 400
database: main
query: |
DELETE FROM feature_flags WHERE name = $1 RETURNING name;
params:
- a|ValidateBody::name|
assert:
http_code_on_error: 404
error_message: "Flag not found"
tests:
- value: count()
is_equal_to: 1
post_transforms:
- extract_value: "[0]"
schema.sqlโ
CREATE TABLE IF NOT EXISTS feature_flags (
id BIGSERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT false,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Seed a few example flags
INSERT INTO feature_flags (name, enabled, description) VALUES
('new_dashboard', false, 'Redesigned dashboard UI'),
('beta_api_v2', false, 'API v2 endpoints'),
('dark_mode', true, 'Dark mode toggle')
ON CONFLICT (name) DO NOTHING;