A minimal feature flag service on AWS — deterministic percentage rollouts, user targeting, and a kill switch. Two HTTP APIs (IAM-authed admin + public evaluator), one DynamoDB table, deployed with AWS CDK. TypeScript, ESM, Node 22.
Topics: feature-flags · feature-toggles · dynamodb · aws-lambda · api-gateway · aws-cdk · serverless · typescript · canary-release · ab-testing · nodejs
Decouple deploying code from releasing features: ship dark, flip on when you're ready, and turn it off in one call when it misbehaves.
(SigV4) (public)
ops / CI ──> Admin API ──┐ ┌── Evaluation API <── your app
(IAM auth) │ │ (cached)
Admin Lambda Evaluate Lambda
│ │
└──> DynamoDB (feature flags) <──┘
- Admin API —
POST/GET/PUT/DELETE /flags, protected with IAM auth. One Lambda routes all CRUD. - Evaluation API —
GET /evaluate, public and latency-sensitive. Reads are cached in-process per warm environment.
| Path | Purpose |
|---|---|
bin/app.ts |
CDK app entry point |
lib/feature-flag-stack.ts |
Table, two Lambdas, two HTTP APIs, IAM |
src/flag.ts |
Shared types |
src/rollout.ts |
Deterministic percentage bucketing — pure, tested |
src/targeting.ts |
Rule matching — pure, tested |
src/repository.ts |
DynamoDB access (create/get/list/update/delete) |
src/http.ts |
JSON response helper |
src/handlers/admin.ts |
Admin CRUD router + validation |
src/handlers/evaluate.ts |
Cached flag evaluation |
test/ |
Unit tests for the pure logic (node:test) |
- Node.js 22+, AWS credentials configured, CDK bootstrapped once:
npx cdk bootstrap
npm install
npm test # pure-logic unit tests, no AWS needed
npx cdk diff
npm run deployDeploy prints both endpoints:
FeatureFlagStack.AdminApiUrl = https://AAAA.execute-api.<region>.amazonaws.com
FeatureFlagStack.EvaluationApiUrl = https://BBBB.execute-api.<region>.amazonaws.com/evaluate
The admin API requires SigV4-signed requests. With AWS SSO (or any temporary
credentials), export them first and always send the session token header —
curl --aws-sigv4 does not add it for you, and without it every call
returns Forbidden:
aws sso login --profile myprofile
eval "$(aws configure export-credentials --profile myprofile --format env)"
ADMIN=https://AAAA.execute-api.<region>.amazonaws.com
REGION=<region>
# create a flag at 20% rollout
curl -X POST "$ADMIN/flags" \
--aws-sigv4 "aws:amz:$REGION:execute-api" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "content-type: application/json" \
-d '{"flagName":"new-dashboard","enabled":true,"rollout":20}'
# widen it to 50% (partial update — other fields untouched)
curl -X PUT "$ADMIN/flags/new-dashboard" \
--aws-sigv4 "aws:amz:$REGION:execute-api" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "content-type: application/json" \
-d '{"rollout":50}'The x-amz-security-token header is required for SSO/temporary credentials and
only skippable with permanent IAM user keys. awscurl handles signing and the
token for you if you'd rather not manage the flags.
The evaluation API is public:
EVAL=https://BBBB.execute-api.<region>.amazonaws.com/evaluate
curl "$EVAL?flag=new-dashboard&userId=12345"
# {"enabled":true,"reason":"rollout"}
# targeting via URL-encoded JSON context
curl "$EVAL?flag=new-dashboard&userId=12345&context=%7B%22country%22%3A%22JP%22%7D"Call /evaluate repeatedly with the same userId — the answer is stable. That's the deterministic hashing at work.
| Variable | Where | Default | Meaning |
|---|---|---|---|
TABLE_NAME |
both Lambdas | (injected by CDK) | Flags table name |
CACHE_TTL_MS |
evaluate Lambda | 10000 |
Flag cache lifetime = kill-switch propagation ceiling |
npm run destroyremovalPolicy is DESTROY for the demo. Switch to RETAIN before real use.