Skip to main content

Feature Flags

Category: Operations ยท ๐Ÿ“ฆ 1 install

Get this pack โ†’

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โ€‹

FilePurpose
config.ymlAirPipe config with docs: true
schema.sqlFeature flags table + seed data

Endpointsโ€‹

MethodPathDescription
GET/flagsList all flags
POST/flags/checkIs a flag enabled?
POST/flags/createCreate a flag
POST/flags/toggleFlip enabled state
POST/flags/setExplicitly set enabled/disabled
POST/flags/deleteRemove 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โ€‹

NameValue
DATABASE_URLyour 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/toggle flips the current state. flags/set is for when you need an explicit value (e.g. from a CI pipeline: enabled: false on deploy, enabled: true after 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;