The dotenv of feature flags.
Define flags in a JSON file. Get typed evaluation anywhere in your codebase.
Zero infrastructure. Zero dependencies.
You want to roll out a feature to 10% of users, or toggle something off without a redeploy. Your options are:
| Option | Problem |
|---|---|
| LaunchDarkly | $500+/month |
| Unleash / Flagsmith | Self-hosted — Docker, databases, maintenance |
Hardcoded if statements |
Scattered across your codebase, no rollout control |
featurekit gives you typed feature flags that read from a JSON file or environment variable. Same user always gets the same result. When you outgrow it, swap the adapter — same API, same code.
npm install featurekit
# or
pnpm add featurekitRequires Node.js 18+
1. Create a flags.json:
{
"newDashboard": true,
"betaFeature": {
"enabled": true,
"percentage": 20,
"users": ["alice@company.com"]
}
}2. Use it:
import { createFlags, fileAdapter } from 'featurekit'
const flags = createFlags({
source: fileAdapter({ path: './flags.json' }),
defaults: { newDashboard: false, betaFeature: false },
})
// Context is automatic via middleware
app.use(flags.middleware())
app.get('/dashboard', async (req, res) => {
if (await flags.isEnabled('newDashboard')) {
return res.json({ dashboard: 'new' })
}
res.json({ dashboard: 'classic' })
})Flag names are fully typed —
flags.isEnabled('typo')is a compile-time error.
Flags can be a simple boolean or a rule-based object:
{
"simpleFlag": true,
"percentageRollout": {
"enabled": true,
"percentage": 25
},
"userTargeted": {
"enabled": true,
"users": ["alice@company.com", "usr_vip_001"]
},
"groupTargeted": {
"enabled": true,
"groups": ["admin", "beta-testers"]
},
"combined": {
"enabled": true,
"percentage": 10,
"users": ["always@enabled.com"],
"groups": ["beta-testers"],
"overrides": {
"blocked-user": false
}
}
}For rule-based flags, evaluation follows a strict priority chain:
| Priority | Rule | Behavior |
|---|---|---|
| 1 | enabled: false |
Flag is off — skip everything |
| 2 | overrides[userId] |
Explicit per-user override (true or false) |
| 3 | users array |
Match on userId or email |
| 4 | groups array |
Match on any group the user belongs to |
| 5 | percentage |
Deterministic hash of flagName:userId — same user, same result |
| 6 | Fallback | Return enabled value |
Reads from a JSON file. Hot-reloads on file changes — no restart needed. If the file becomes invalid JSON, it logs a warning and keeps the previous valid state.
import { fileAdapter } from 'featurekit'
const source = fileAdapter({ path: './flags.json' })Reads flags from a FEATUREKIT_FLAGS environment variable. Works everywhere — serverless, edge, containers.
import { envAdapter } from 'featurekit'
const source = envAdapter()
// or with a custom env var:
const source = envAdapter({ envVar: 'MY_FLAGS' })Takes a plain object. Ideal for testing.
import { memoryAdapter } from 'featurekit'
const source = memoryAdapter({
newDashboard: true,
betaFeature: false,
})featurekit uses AsyncLocalStorage to propagate user context automatically. Set it once in middleware, read it anywhere — no argument threading through your service layers.
app.use(flags.middleware())Pass a custom extractor for your auth setup:
const flags = createFlags({
source: fileAdapter({ path: './flags.json' }),
defaults: { newDashboard: false },
context: {
extract: (req) => ({
userId: req.auth.sub,
email: req.auth.email,
groups: req.auth.roles,
}),
},
})Use runWithContext when middleware isn't available:
import { runWithContext } from 'featurekit'
await runWithContext(
{ userId: 'usr_123', groups: ['beta'] },
async () => {
const enabled = await flags.isEnabled('betaFeature')
// ...
}
)Skip the automatic context and pass it directly:
await flags.isEnabled('betaFeature', { userId: 'usr_123' })Useful for bootstrapping a frontend:
const allFlags = await flags.getAll()
// → { newDashboard: true, betaFeature: false }Use the memory adapter to control flag state in tests without touching files or env vars:
import { createFlags, memoryAdapter } from 'featurekit'
const flags = createFlags({
source: memoryAdapter({ newDashboard: true, betaFeature: false }),
defaults: { newDashboard: false, betaFeature: false },
})
// Assert behavior under specific flag states
expect(await flags.isEnabled('newDashboard')).toBe(true)| Method | Description |
|---|---|
createFlags({ source, defaults }) |
Create a typed featurekit instance |
flags.isEnabled(name, ctx?) |
Check if a flag is enabled for the current user |
flags.getAll(ctx?) |
Evaluate all flags at once |
flags.middleware() |
Express/Fastify/Koa middleware for automatic context |
flags.runWithContext(ctx, fn) |
Run a function with explicit user context |
flags.destroy() |
Stop watching and clean up resources |
See CONTRIBUTING.md for setup instructions and guidelines.