featurekit
v0.1.1
Published
The dotenv of feature flags. Zero infrastructure, fully typed.
Maintainers
Readme
Why
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.
Install
npm install featurekit
# or
pnpm add featurekitRequires Node.js 18+
Quick Start
1. Create a flags.json:
{
"newDashboard": true,
"betaFeature": {
"enabled": true,
"percentage": 20,
"users": ["[email protected]"]
}
}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.
Flag Schema
Flags can be a simple boolean or a rule-based object:
{
"simpleFlag": true,
"percentageRollout": {
"enabled": true,
"percentage": 25
},
"userTargeted": {
"enabled": true,
"users": ["[email protected]", "usr_vip_001"]
},
"groupTargeted": {
"enabled": true,
"groups": ["admin", "beta-testers"]
},
"combined": {
"enabled": true,
"percentage": 10,
"users": ["[email protected]"],
"groups": ["beta-testers"],
"overrides": {
"blocked-user": false
}
}
}Evaluation Priority
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 |
Adapters
File Adapter
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' })Environment Variable Adapter
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' })Memory Adapter
Takes a plain object. Ideal for testing.
import { memoryAdapter } from 'featurekit'
const source = memoryAdapter({
newDashboard: true,
betaFeature: false,
})Context Propagation
featurekit uses AsyncLocalStorage to propagate user context automatically. Set it once in middleware, read it anywhere — no argument threading through your service layers.
Express / Fastify / Koa
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,
}),
},
})Background Jobs / Next.js App Router
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')
// ...
}
)Explicit Context Override
Skip the automatic context and pass it directly:
await flags.isEnabled('betaFeature', { userId: 'usr_123' })Get All Flags
Useful for bootstrapping a frontend:
const allFlags = await flags.getAll()
// → { newDashboard: true, betaFeature: false }Testing
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)API Reference
| 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 |
Contributing
See CONTRIBUTING.md for setup instructions and guidelines.
