Tower is a full-stack application that handles the functionality related to feature flag management.
It consists of a React user interface that allows a user to perform basic CRUD functionality, such as creating an app and flag, making edits to flags, toggling a flag on and off, and viewing logs related to those flags.
Tower’s backend is written in Node.js and Express. Tower is configured to connect to the database, and upon any change to the feature flag data, Tower updates the database and publishes the full set of feature flag ruleset data to NATS JetStream.
Clone main branch of repository
Sample .env
file to add into the server directory
PORT=3001
PGHOST='localhost'
PGUSER='postgres'
PGDATABASE='tower'
PGPASSWORD='secret'
PGPORT=5432
NATS_SERVER='nats://127.0.0.1:4222'
SDK_KEY='myToken'
REDIS_SERVER='{"socket":{"host":"localhost"}}'
Sample .env
file to add into the client directory
REACT_APP_NATS_WS_SERVER='ws://0.0.0.0:8080'
REACT_APP_SDK_KEY='myToken'
Install NATS
From Tower root directory in separate terminal run nats-server -c nats.conf
to stop a nats server run nats-server --signal quit
to delete stream messages run nats stream purge
Install Redis
From any directory in separate terminal run docker run -p 6379:6379 -it --rm redislabs/redistimeseries
Install PostgreSQL
From any directory run brew services start postgresql
From the server directory npm install
From the server directory run npm run dev
The backend app is now available at http://localhost:3001
From the server directory npm install
From the server directory run npm start
The frontend app is now available at http://localhost:3000
All requests and responses are in json
Returns all apps
Example Response:
{
"payload": [
{
"id": 1,
"title": "First app",
"created_at": "2022-08-11T18:13:55.106Z",
"updated_at": "2022-08-11T18:13:55.106Z"
},
{
"id": 2,
"title": "Second App",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:27:17.068Z"
}
]
}
Returns the app with a matching appId
Example Response:
{
"payload": {
"id": 2,
"title": "Second App",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:27:17.068Z"
}
}
Creates a new app
Example Request:
{
"title": "First App"
}
Example Response:
{
"payload": {
"title": "First App",
"id": 1,
"created_at": "2022-08-11T18:27:17.068Z"
}
}
Updates the app with a matching appId
Example Request:
{
"title": "App with Edited name"
}
Example Response:
{
"payload": {
"id": 2,
"title": "App with Edited name",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:42:19.981Z"
}
}
Deletes the app with a matching appId
Example Response:
{
"payload": {
"id": 2,
"title": "App with Edited name",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:42:19.981Z"
}
}
Returns all flags ruleset data belonging to a specific app
Example Response:
{
"payload": [
{
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 100,
"white_listed_users": "",
"error_threshold_percentage": 50,
"circuit_status": "open",
"is_recoverable": false,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"webhooks": "",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:51:52.882Z"
},
{
"id": 2,
"app_id": 1,
"title": "App 1 Flag 2",
"description": "",
"is_active": false,
"rollout_percentage": 100,
"white_listed_users": "",
"error_threshold_percentage": 50,
"circuit_status": "open",
"is_recoverable": false,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"webhooks": "",
"created_at": "2022-08-11T18:52:27.200Z",
"updated_at": "2022-08-11T18:52:27.200Z"
}
]
}
Returns the flag with a matching flagId
and its ruleset data
Example Response:
{
"payload": {
"id": 1,
"title": "App 1 Flag 1",
"app_id": 1,
"is_active": false,
"description": "",
"rollout_percentage": 100,
"white_listed_users": "",
"webhooks": "",
"circuit_status": "open",
"is_recoverable": false,
"error_threshold_percentage": 50,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:51:52.882Z",
"logs": [
{
"log_id": 1,
"flag_id": 1,
"log_description": "Flag Created",
"action_type": "create",
"created_at": "2022-08-11T18:51:52.887Z",
"updated_at": "2022-08-11T18:51:52.887Z"
}
]
}
}
Creates a new flag
Successfully creating a flag with POST
will also create a new log. All flags are initially created in an 'off' toggle state.
Example Request:
{
"title": "App 1 Flag 1",
"rollout_percentage": 100,
"circuit_recovery_percentage": 100,
"error_threshold_percentage": 50
}
Example Response:
{
"payload": {
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 100,
"white_listed_users": "",
"error_threshold_percentage": 50,
"circuit_status": "open",
"is_recoverable": false,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"webhooks": "",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:51:52.882Z"
}
}
Updates the flag with a matching flagId
Successfully updating a flag with PATCH
will create a new log.
Example Request:
{
"title": "App 1 Flag 1",
"rollout_percentage": 50,
"circuit_status": "close",
"is_active": false,
"is_recoverable": true,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_increment_percentage": 10,
"circuit_recovery_percentage": 10,
"error_threshold_percentage": 40,
"circuit_recovery_rate": 400,
"circuit_recovery_profile": "linear",
"circuit_recovery_delay": 8000,
"webhooks": "https://hooks.slack.com/services/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"
}
Example Response:
{
"payload": {
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 50,
"white_listed_users": "",
"error_threshold_percentage": 40,
"circuit_status": "open",
"is_recoverable": true,
"circuit_recovery_percentage": 10,
"circuit_recovery_delay": 8000,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 400,
"circuit_recovery_increment_percentage": 10,
"circuit_recovery_profile": "linear",
"webhooks": "https://hooks.slack.com/services/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",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:56:12.441Z"
}
}
Toggles the is_active
state of a flag with a matching flagId
on or off
Successfully toggling a flag with PATCH
will create a new log.
Example Response:
{
"payload": {
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 50,
"white_listed_users": "",
"error_threshold_percentage": 40,
"circuit_status": "open",
"is_recoverable": true,
"circuit_recovery_percentage": 10,
"circuit_recovery_delay": 8000,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 400,
"circuit_recovery_increment_percentage": 10,
"circuit_recovery_profile": "linear",
"webhooks": "https://hooks.slack.com/services/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",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T19:03:47.457Z"
}
}
Deletes the flag with a matching flagId
Successfully deleting a flag with DELETE
will create a new log.
Example Response:
{
"payload": 1
}
Trips the circuit of a flag with a matching flagId
open
Successfully opening a circuit with POST
will create a new log.
Example Response:
{
"payload": 1
}
Trips the circuit of a flag with a matching flagId
closed
Successfully closing a circuit with POST
will create a new log.
Example Response:
{
"payload": 1
}
Returns all logs
Example Response:
{
"payload": [
{
"log_id": 4,
"flag_id": 1,
"flag_title": "App 1 Flag 1",
"log_description": "Circuit Tripped Open",
"action_type": "circuit_open",
"created_at": "2022-08-11T19:27:29.104Z",
"updated_at": "2022-08-11T19:27:29.104Z"
},
{
"log_id": 3,
"flag_id": 1,
"flag_title": "App 1 Flag 1",
"log_description": "Circuit Closed",
"action_type": "circuit_close",
"created_at": "2022-08-11T19:25:36.826Z",
"updated_at": "2022-08-11T19:25:36.826Z"
},
{
"log_id": 2,
"flag_id": 2,
"flag_title": "App 1 Flag 2",
"log_description": "Flag Created",
"action_type": "create",
"created_at": "2022-08-11T19:25:13.614Z",
"updated_at": "2022-08-11T19:25:13.614Z"
},
{
"log_id": 1,
"flag_id": 1,
"flag_title": "App 1 Flag 1",
"log_description": "Flag Created",
"action_type": "create",
"created_at": "2022-08-11T19:25:05.345Z",
"updated_at": "2022-08-11T19:25:05.345Z"
}
]
}
Returns all logs belonging to an app with a matching appId
Example Response:
{
"payload": [
{
"id": 1,
"flag_id": 1,
"app_id": 1,
"description": "Flag Created",
"action_type": "create",
"flag_title": "App 1 Flag 1",
"flag_description": "",
"created_at": "2022-08-11T19:25:05.345Z",
"updated_at": "2022-08-11T19:25:05.345Z"
},
{
"id": 3,
"flag_id": 1,
"app_id": 1,
"description": "Circuit Closed",
"action_type": "circuit_close",
"flag_title": "App 1 Flag 1",
"flag_description": "",
"created_at": "2022-08-11T19:25:36.826Z",
"updated_at": "2022-08-11T19:25:36.826Z"
}
]
}
Returns a SDK key
Example Response:
{
"payload": {
"id": "70a90b16-201c-4466-8dc9-5f5c620d5732",
"created_at": "2022-08-11T19:37:46.595Z",
"updated_at": "2022-08-11T19:37:46.595Z"
}
}
Creates a SDK key
Example Response:
{
"payload": {
"id": "3e381b25-7686-4847-b861-5ec9b87378f1",
"created_at": "2022-08-11T19:37:13.391Z",
"updated_at": "2022-08-11T19:37:13.391Z"
}
}
Returns success/failure counts for a specified flag grouped into time buckets within a selected window of time
{
"payload": {
[
{ "timestamp": 1661264546556, "success": 137, "failure": 4 },
{ "timestamp": 1661264549556, "success": 134, "failure": 6 },
{ "timestamp": 1661264552556, "success": 138, "failure": 3 },
{ "timestamp": 1661264555556, "success": 136, "failure": 5 },
{ "timestamp": 1661264558556, "success": 131, "failure": 9 },
{ "timestamp": 1661264561556, "success": 135, "failure": 6 },
{ "timestamp": 1661264564556, "success": 130, "failure": 10 },
{ "timestamp": 1661264567556, "success": 130, "failure": 11 },
{ "timestamp": 1661264570556, "success": 137, "failure": 3 },
{ "timestamp": 1661264573556, "success": 132, "failure": 9 }
]
}
}