Flip features, not deploys.
New checkout flow
new-checkout · boolean
Rider.city == "BANGALORE" and Rider.age > 20
- checkout-apinew-checkout · v14on
- rider-appnew-checkout · v14on
- pricing-servicenew-checkout · v14on
Version 14 of new-checkout is enabled on all 3 services.
- to check a flag whose rule is a single comparison
- ~400 ns
- network calls when a service checks a flag
- 0
- rule engine, shared by the server and the SDK
- 1
- flag types: boolean, string, number and JSON
- 4
Measured on an Intel Core i5-13400F with Go 1.27. Full benchmark report
Change it once. Every service hears about it.
- 01
Define flags and rules
Create a flag in an environment and write the rule that turns it on, such as enabling the new checkout only for riders in Bangalore.
- 02
Beacon broadcasts the change
Every create and update is pushed over a Server-Sent Events stream to each connected service. No polling, no redeploy, no restart.
- 03
Services evaluate locally
The SDK keeps the latest flags and compiled rules in memory. Checking a flag is a function call, and a service keeps its last known flags if the connection drops.
Targeting rules you can read out loud.
Compare
==><>=<=Numbers and strings, reached with dotted paths like Rider.address.city.
Match lists
in one ofin all ofCheck a value against a list, or require an array of tags to contain every one.
Combine
andornot(…)( )Group conditions with parentheses. Evaluation stops as soon as the answer is known.
Fail loudly
Syntax errors, missing fields and type mismatches come back as errors you can handle.
{ "Rider": { "city": "BANGALORE", "age": 34, "tags": ["vip"] } }
Rider.city == "BANGALORE" and Rider.age > 20trueRider.city in one of ["BANGALORE", "DELHI"]trueRider.tags in all of ["vip", "new"]false(Rider.age < 25 or Rider.age > 60) and not(Rider.city == "MUMBAI")falseRider.plan == "pro"unknown identifier
The server and the SDK share one engine, so a rule gives the same answer wherever it runs. A missing field is an error, never a silent false.
Everything around the flag, built in.
Projects and environments
Group flags by project and give each environment its own state and rule. Flag and environment keys never change after creation, so code that references them keeps working.
Four flag types
Serve more than on and off: a string variant, a numeric limit or a whole JSON config.
Percentage and gradual rollouts
Release to a share of traffic, or ramp up step by step, on top of your targeting rule.
Scheduled windows
Give a flag a start and an end time, and it switches on and off by itself.
Version history
Every rule change saves a snapshot of the flag, so you can see who changed what, and when.
Roles and permissions
Give each person a role on a project and, where needed, a different one per environment. Deny a single permission and it beats every grant.
Scoped API keys
Services authenticate with per-project keys that carry only the permissions they need. Keys are stored hashed and can expire.
Google and GitHub sign-in
Teammates sign in with an account they already have.
Email invitations
Invite people to a project with a role. Invitations expire on their own.
Stale flag detection
SDKs report when each flag was last evaluated, so dead flags stand out.
Keeps working offline
A service that loses its connection keeps serving its last known flags.
Building in the open.
- Rule language, compiler and evaluatorDone
- HTTP server with POST /evaluateDone
- Flag and rule management APIPlanned
- SSE update streamPlanned
- Service SDKPlanned
$ cd control-plane-server && go run ./cmd $ curl -X POST localhost:8080/evaluate \ -H 'Content-Type: application/json' \ -d '{ "rule": "Rider.city == \"Bangalore\" and Rider.age > 20", "data": {"Rider": {"city": "Bangalore", "age": 40}} }' {"data": {"result": true}, "error": null}
Start the server, then send the request from a second terminal.
Ship the code once. Decide who sees it later.
Clone the repository, start the server and evaluate your first rule in a couple of minutes.