Flaghoist
OpenFeature-native feature flags you host yourself. It runs on your infrastructure, with a dashboard, a CLI and an MCP server.
- Licence
- Apache-2.0
- Status
- Live, pre-1.0
- Languages verified live
- 7
- Storage adapters
- 5
- Packages on npm
- 14
- Downloads, last month
- about 4,300
What it is#
Flaghoist is a feature-flag service you run yourself. npm create flaghoist writes a config file, npx flaghoist deploy puts it on Cloudflare Workers, and one deploy gives you a read API, an admin API and a dashboard. Your apps talk to it through the OpenFeature SDKs they already use. There is a live demo that resets every hour.
Why it exists#
A feature flag is a small document you read on a hot path. Hosted services are priced and built for far more than that, and the self-hosted options usually mean an always-on server and a database for something that changes a few times a week.
So the design goals were narrow. Nothing running when nobody is asking. Storage you already pay for. A standard protocol, so nobody is locked into my SDKs.
The decisions#
Speak OFREP instead of shipping SDKs#
The server implements the read path of the OpenFeature Remote Evaluation Protocol (OFREP). Any OpenFeature SDK with an OFREP provider can talk to it without a Flaghoist-specific package. Seven languages supported from the start: JavaScript (TypeScript), Go, Python, Ruby, Java, Rust and .NET.
Go failed once, and it was the interesting one. For a disabled flag the server sent value: false, reason: DISABLED. The JavaScript provider honoured the value. The Go provider read DISABLED as “this flag is not participating, use your own default”. A service written as BooleanValue(ctx, "feature", true) kept serving a feature that had been switched off while the dashboard showed it off. That is the kill-switch case, where being wrong costs the most.
The fix was to report STATIC on the wire. evaluate() still returns DISABLED internally and the admin API did not change. One provider reading the spec differently from another stays invisible until you put two of them side by side against the same server, which is the reason to keep running the other five.
Under a wrong API key, every provider returned the caller’s default rather than a real value. None failed open.
Sticky rollouts, with nothing stored#
A percentage rollout needs a stable answer. If a user is in the rollout on one page load and out of it on the next, the feature flickers. The usual fix is an assignment table, which is state to store and keep in sync across every edge location.
Flaghoist stores nothing. A user’s bucket is the first four bytes of SHA-256("<targetingKey>:<flagKey>"), taken modulo 100, and they are in the rollout when their bucket is below the percentage. Three things follow. The same user and flag always give the same answer. Raising the rollout from 25% to 40% only ever adds people. And because the flag key is part of the seed, different flags do not all pick the same early users.
SHA-256 is only doing determinism and an even spread here; its cryptographic strength is incidental. The demo below runs the same arithmetic on the same inputs.
A storage interface small enough to verify#
The adapter interface has four required methods:
interface StorageAdapter {
get(key: string): Promise<FeatureFlag | null>
put(key: string, flag: FeatureFlag): Promise<void>
delete(key: string): Promise<void>
list(): Promise<FeatureFlag[]>
}
Audit-log and webhook methods exist too, but they are optional and the server falls back to memory. Four methods is small enough that Cloudflare KV, Redis, Postgres, SQLite and an in-memory adapter all fit. It is also small enough to write one shared conformance suite that every adapter must pass, so “bring your own database” is backed by tests.
14 packages#
The evaluation engine, @flaghoist/core, has zero dependencies. The server, each storage adapter, each OpenFeature provider, the Vue bindings, the CLI, the MCP server and the admin client that the CLI, dashboard and MCP server share are each their own package. An app installs only what it uses, and the engine has nothing to inherit. Versioning and changelogs run through Changesets.
The cost is coordination. A change to core ripples into most of the others, and 14 version numbers have to stay in step.
What it does not do yet#
| Flaghoist today | |
|---|---|
| Flag types | Boolean only. No multivariate flags yet. |
| Experiments and A/B tests | No. Bring your own analytics. |
| Maturity | Pre-1.0, one maintainer. |
The project is built so that matters less than it sounds. It is Apache-2.0, so anyone can fork it. Your flags live in your own storage. The API is an open standard, so the same providers work against another OFREP server. And flaghoist eject hands you a self-contained TypeScript project. If it stalled tomorrow, you would keep running exactly what you run today.