Flags guides
10 · Serverless and edge

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.

handler.ts
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:

worker.ts
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");
  },
};