Storage adapters
Storage is the “bring your own DB” seam. Every adapter implements the same four-method interface, and every adapter, shipped or yours, must pass the shared conformance suite.
interface StorageAdapter { get(key: string): Promise<FeatureFlag | null> put(key: string, flag: FeatureFlag): Promise<void> delete(key: string): Promise<void> list(): Promise<FeatureFlag[]>}Cloudflare KV (default)
Section titled “Cloudflare KV (default)”import { cloudflareKV } from '@flaghoist/adapter-cloudflare-kv'
createFlagServer((env) => ({ storage: cloudflareKV(env.FLAGS), auth: {/* … */} }))Works with ioredis (Node) and Upstash (edge). All flags live in one hash, so list() is a single
hgetall.
import { redisAdapter } from '@flaghoist/adapter-redis'import Redis from 'ioredis'
createFlagServer({ storage: redisAdapter(new Redis(process.env.REDIS_URL)), auth: {/* … */} })Postgres
Section titled “Postgres”Stores flags in a jsonb table via any node-postgres client. Run the schema once:
import { initPostgres, postgresAdapter } from '@flaghoist/adapter-postgres'import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })await initPostgres(pool) // CREATE TABLE IF NOT EXISTS flaghoist_flags (...)
createFlagServer({ storage: postgresAdapter(pool), auth: {/* … */} })Memory
Section titled “Memory”Backed by a Map, with no persistence. Flags are lost on restart, so this is for local
development, tests, and npx flaghoist init --storage memory when you just want something running
immediately with nothing to provision.
import { memoryAdapter } from '@flaghoist/adapter-memory'
createFlagServer({ storage: memoryAdapter(), auth: {/* … */} })Seed it with flags at startup by passing them to the factory. createFlag fills in the metadata a
hand-built object would otherwise be missing:
import { createFlag } from '@flaghoist/core'
memoryAdapter([createFlag({ key: 'new-checkout', enabled: true, rollout: { percentage: 25 } })])Writing your own
Section titled “Writing your own”Implement the four methods. A key maps to one flag’s JSON, so any store fits. Re-validate reads
through parseFlag so corrupt data degrades to “flag ignored” rather than crashing evaluation:
import { parseFlag, type StorageAdapter } from '@flaghoist/core'
export function myAdapter(client: MyClient): StorageAdapter { return { async get(key) { const raw = await client.read(key) return raw ? parseFlag(JSON.parse(raw)) : null }, async put(key, flag) { await client.write(key, JSON.stringify(flag)) }, async delete(key) { await client.remove(key) }, async list() { return (await client.all()).map((r) => parseFlag(JSON.parse(r))).filter(Boolean) }, }}Prove it against the conformance suite:
import { testStorageAdapter } from '@flaghoist/adapter-conformance'testStorageAdapter('my-db', () => myAdapter(freshClient()))For SQL adapters, use parameterized queries everywhere, and validate any table/identifier names: they cannot be parameterized and are the one place injection could enter.