Feature Flags and Entitlements
Feature flag services give you a dashboard and a fixed menu of targeting options: on, off, a percentage, a list of users, maybe an attribute match. The moment a rule doesn't fit the menu ("Team plans, plus anyone in the beta, but not in the EU yet"), it ends up in application code, split across the server and the front end.
With Stof, each flag is a small function that decides whether it's on for a user. The flags are one document, evaluated in your process, on the server and in the browser alike, and changed by publishing a new version.
The Flags
Each flag is a function tagged #[flag] that takes a user. A helper gives every user a stable number from 0 to 99 for percentage rollouts. Press Run to evaluate the flags for two users and check the rollout across 1,000 users:
/// People trying features early.
beta: ['u_7', 'u_19']
/// New checkout: US only, rolling out to 25% of users.
#[flag]
fn new_checkout(user: obj) -> bool {
user.country == 'US' && self.bucket(user.id, 'new_checkout') < 25
}
/// The AI assistant is a paid feature.
#[flag]
fn ai_assistant(user: obj) -> bool {
user.plan == 'pro' || user.plan == 'team'
}
/// Bulk export: Team plans, and anyone in the beta.
#[flag]
fn bulk_export(user: obj) -> bool {
user.plan == 'team' || self.beta.contains(user.id)
}
/// A number from 0 to 99 that's the same for a user every time, and different for each flag.
fn bucket(user_id: str, flag: str) -> int {
let hash = 0;
for (const byte in (flag + ':' + user_id) as blob) hash = (hash * 31 + byte) % 1_000_003;
hash % 100
}
/// Every flag that's on for a user.
fn flags_for(input: map) -> list {
using user = Obj::from_map(input);
const on = [];
for (const flag in self.funcs('flag')) {
if (flag(user)) on.push_back(flag.name());
}
on.sort();
on
}
#[main]
fn main() {
pln(self.flags_for({'id': 'u_7', 'plan': 'free', 'country': 'US'}));
pln(self.flags_for({'id': 'u_8', 'plan': 'team', 'country': 'DE'}));
let rolled_out = 0;
for (const i in 1000) {
if (self.new_checkout(using new { id: 'u_' + i, country: 'US' })) rolled_out += 1;
}
pln(`new_checkout is on for ${rolled_out} of 1000 US users`);
}A flag can be as simple as true or as specific as your business needs, and each one carries a comment that says why it exists. Plan entitlements, like ai_assistant, sit right next to experiments, so "what can this user do?" has one answer.
On the Server
Load the flags once, and evaluate them for each request. Every flag for a user takes well under a millisecond:
import { StofDoc } from '@formata/stof';
let flags = await StofDoc.new(await fetchPublishedFlags(), 'bstf');
flags.setMaxExecutionTime(20);
app.use(async (req, res, next) => {
req.flags = new Set(await flags.call('flags_for', {
id: req.user.id, plan: req.account.plan, country: req.geo.country,
}) as string[]);
next();
});
app.post('/export', (req, res) => {
if (!req.flags.has('bulk_export')) return res.status(403).end();
// ...
});
In the Browser
The npm package runs in the browser too, so the front end can load the same document and evaluate flags locally, with no request per check and no flicker while flags load:
const flags = await StofDoc.new(new Uint8Array(await (await fetch('/flags.bstf')).arrayBuffer()), 'bstf');
const on = await flags.call('flags_for', currentUser) as string[];
The browser gets the whole document, so it can see every rule and list in it. For flags with sensitive targeting, like a list of customer names, evaluate on the server and send the result instead.
Changing Flags
Flags change the same way pricing plans do: edit the document, publish it as binary Stof, and every service and browser swaps it in. Turning a feature off in an emergency is a one-line change:
#[flag]
fn new_checkout(user: obj) -> bool { false }
Keep the flags in version control with a few #[test] functions ("Team users get bulk export", "nobody outside the US gets the new checkout"), and publish from CI, so every flag change is reviewed and tested like code.
Why This Works
- Any rule you can write. Targeting isn't limited to a menu of options: it's a function, with access to everything you pass in.
- The same answer everywhere. Server, browser, and workers evaluate one document with one runtime.
- Fast and local. No network call per check, and nothing to wait for on page load.
- Reviewed like code. Flags are text with comments and tests, in version control.
Plan entitlements, kill switches, gradual rollouts, and A/B assignments all fit this shape. For limits and billing on top of entitlements, see Usage Limits and Pricing Plans.