Your first flag.
Create a flag, read it from Node, switch it in the console and watch your app follow. About ten minutes.
You need a ProductCraft workspace (sign up at console.productcraft.co) and Node.js 20.3 or later. Flags is hosted in the EU (DigitalOcean, Frankfurt, fra1). It is free during the preview, with no SLA; our internal target for config delivery is 99.5% a month, and the setup below keeps your app working through an outage anyway.
1
Turn Flags on
A workspace created since 10 November 2026 has Flags on already. In an older one, open Workspace › Services in the console and enable Flags. Until it is on, the management API answers 403 SERVICE_NOT_ENABLED. Turning it off later blocks the management API but keeps serving configs, so running apps are never cut off.
2
Create a project and a flag
Open Flags in the console and create a project, for example “Storefront”. It comes with a Development and a Production environment and one config, “Main”. Add a feature flag with the key new_checkout (type boolean). It starts off in every environment.
Then open SDK keys, pick Main and Development, and reveal the key (it starts with pcf_sdk_). Put it in your server's environment as PRODUCTCRAFT_FLAGS_SDK_KEY. This key can only read this one config in this one environment.
3
Install and create the client
npm install @productcraft/flags ioredisCreate one client per process. It downloads the config, keeps it fresh every 60 seconds and evaluates flags locally. Two lines of defence are on from the start, as we recommend for production:
- A cache (here Redis): every config the client downloads is saved, so a process that starts while Flags is unreachable starts from the last config instead of your defaults.
- A bootstrap: a config JSON file your build ships with, used only when neither the network nor the cache has one. Fail-safe and caching shows the CI step that writes it. Run that script once before your first start, or leave the
bootstrapline out until CI writes the file (an empty or invalid file is ignored with a warning).
// lib/flags.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!);
// Any store with get/set works: Redis, a KV namespace, a file.
// The cache keys never contain the SDK key.
const cache: FlagsCache = {
get: (key) => redis.get(key),
set: async (key, value) => {
await redis.set(key, value);
},
};
// One client per process. It downloads the config every 60 s
// (If-None-Match, so an unchanged config costs a 304) and evaluates
// every flag locally: user attributes never leave your app.
export const flags = new FlagsClient({
sdkKey: process.env.PRODUCTCRAFT_FLAGS_SDK_KEY,
cache, // a restart during an outage starts from the last config
bootstrap, // the last line of defence: the config your build shipped with
});4
Read the flag
Pass the default you want when anything goes wrong and the user you are evaluating for. getValue never throws: until a config is there, or if the flag is missing or of another type, you get your default back.
const user = {
identifier: "user-123", // the attribute percentage rollouts hash
email: "ada@example.com",
country: "NL",
custom: { plan: "pro" },
};
// The default (false) is returned until a config is there, and on any
// failure: getValue never throws.
if (await flags.getValue("new_checkout", false, user)) {
console.log("new checkout");
} else {
console.log("old checkout");
}
// On shutdown: stop the poll timer.
flags.close();Run it: it prints old checkout, because the flag is off in Development.
5
Turn it on and watch it arrive
Switch new_checkout on for Development in the console and save. Your app picks it up on its next poll: within about 71 seconds at the default 60-second interval (poll interval × 1.1 + 5 s). To see it sooner, refresh by hand; the delivery endpoint has a saved change within about 5 seconds:
flags.on("configChanged", (e) => console.log("config revision", e.revision));
// Turned it on in the console? Fetch now instead of waiting up to ~71 s
// (delivery has a saved change within about 5 s).
await flags.forceRefresh();
console.log(await flags.getValue("new_checkout", false, { identifier: "user-123" }));6
Or do steps 2 to 5 from code
The same package has a management client for scripts and CI. It uses a workspace API key, never an SDK key.
Create a workspace API key in Workspace › API keys with the “Manage flags” preset, plus flags.sdk_key.reveal if the script reads SDK keys. Set PRODUCTCRAFT_API_KEY and PRODUCTCRAFT_WORKSPACE_ID.
import { Flags } from "@productcraft/flags";
// Reads PRODUCTCRAFT_API_KEY (a workspace API key with the Flags
// actions it needs) and PRODUCTCRAFT_WORKSPACE_ID.
const flags = new Flags();
const project = await flags.projects.create({ name: "Storefront", environment_preset: "dev_prod" });
const config = project.configs[0]!; // "Main"
const development = project.environments.find((e) => e.key === "development")!;
const setting = await flags.configs.settings.create(config.id, {
key: "new_checkout",
name: "New checkout",
type: "boolean",
});
// Turn it on in Development. `version` is optimistic concurrency:
// send back what you read, a stale one answers 409 VERSION_CONFLICT.
const current = setting.values.find((v) => v.environment_id === development.id)!;
await flags.settings.values.update(setting.id, development.id, {
version: current.version,
value: true,
note: "Try the new checkout in development",
});
// The SDK key of Main in Development (needs flags.sdk_key.reveal).
const keys = await flags.configs.environments.sdkKeys.reveal(config.id, development.id);
console.log(keys.data[0]!.key.slice(0, 12) + "…");Next
Where to go from here
- Core concepts: the user object, rules, comparators, segments and what an SDK key exposes.
- Percentage rollouts and the kill-switch runbook.
- React and Next.js or serverless and edge.
- API reference.