Functions and the edge.
No timers between invocations and cold starts everywhere: lazy polling with a shared cache, or the engine on its own.
1
Serverless functions
Use lazy polling: the client downloads only when its config is older than ttlSeconds, at the moment a flag is read. With a shared cache (Redis, a KV store), a cold start reads the cache in one round trip and downloads nothing while the cached config is fresh, so only one invocation per TTL goes to Flags, with If-None-Match.
import { FlagsClient, type FlagsCache } from "@productcraft/flags";
import Redis from "ioredis";
import bootstrap from "./flags-bootstrap.json";
const redis = new Redis(process.env.REDIS_URL!);
const cache: FlagsCache = {
get: (key) => redis.get(key),
set: async (key, value) => {
await redis.set(key, value);
},
};
// Module scope: reused while the instance stays warm.
const flags = new FlagsClient({
sdkKey: process.env.PRODUCTCRAFT_FLAGS_SDK_KEY,
polling: { mode: "lazy", ttlSeconds: 60 }, // no timers between invocations
cache, // a cold start reads Redis: no download while it is younger than the TTL
bootstrap,
});
export async function handler(event: { userId: string }) {
const enabled = await flags.getValue("new_checkout", false, { identifier: event.userId });
return { statusCode: 200, body: JSON.stringify({ enabled }) };
}Propagation is then at most the TTL plus 5 seconds after the cache expires. The bootstrap covers a cold start during an outage when the cache is empty.
2
Edge runtimes
Where node: modules are missing, @productcraft/flags-engine evaluates on its own: no dependencies, no network code, synchronous (its SHA-1 and SHA-256 are built in, because Web Crypto is async). You fetch the config, keep a copy in your KV store, and evaluate:
import { evaluate, parseConfig, type ParsedConfig } from "@productcraft/flags-engine";
// A KV namespace your platform gives you (Cloudflare KV, Vercel KV …).
declare const KV: { get(key: string): Promise<string | null>; put(key: string, value: string): Promise<void> };
declare const SDK_KEY: string;
let held: { config: ParsedConfig; at: number } | null = null;
async function config(): Promise<ParsedConfig | null> {
if (held && Date.now() - held.at < 60_000) return held.config;
try {
const res = await fetch(`https://api.flags.productcraft.co/v1/configs/${SDK_KEY}/config.json`);
if (res.ok) {
const body = await res.text();
held = { config: parseConfig(body), at: Date.now() };
await KV.put("flags-config", body);
return held.config;
}
} catch {
// fall through to the copy in KV
}
const saved = await KV.get("flags-config");
return saved ? parseConfig(saved) : (held?.config ?? null);
}
export default {
async fetch(request: Request): Promise<Response> {
const userId = new URL(request.url).searchParams.get("user") ?? "";
const cfg = await config();
const on = cfg ? evaluate(cfg, "new_checkout", false, { identifier: userId }).value : false;
return new Response(on ? "new" : "old");
},
};