@guidelytech/flags
v0.8.0
Published
Guidely feature flags and experiments for TypeScript: browsers, Node.js, Next.js, edge runtimes
Maintainers
Readme
@guidelytech/flags
Guidely feature flags, remote config and experiments for TypeScript. One client for browsers, Node.js ≥ 18, Next.js (server and edge), Deno, Bun and React Native; no runtime dependencies.
If you already use a Guidely SDK (@guidelytech/react, nextjs, react-native, svelte,
sveltekit, sdk-core or the script tag), flags are built in: guidely.flags, useFlag(),
flag()… with identity, page and device attributes filled in for you. Use this package directly
for servers, edge functions and flags-only apps.
pnpm add @guidelytech/flagsKeys
- Public key
pk_…— the environment key you already use asprojectIdfor tracking. Works everywhere, with no other configuration. - Secret key
gly_sk_…(scopeflags:read, servers only) — also needsenvironment. Gets server-only flags, and works when the environment hides its rules from public keys.
Browser
import { createFlagsClient } from "@guidelytech/flags";
const flags = createFlagsClient({
sdkKey: "pk_prod_…",
apiUrl: "https://guidelytech.com",
attributes: { id: user.id, plan: user.plan, account: { id: org.id } },
});
await flags.ready();
if (flags.isEnabled("new-checkout")) showNewCheckout();
const color = flags.getString("checkout-button-color", "blue");
flags.subscribe(() => render()); // payload, attribute or override changes
flags.updateAttributes({ plan: "enterprise" });The last payload is kept in localStorage, so returning visitors get flags instantly and offline.
Node.js / server
One client per process; evaluate per request with forUser() (it never changes the client):
import { createFlagsClient } from "@guidelytech/flags";
export const flags = createFlagsClient({ sdkKey: process.env.GUIDELY_SDK_KEY!, environment: "production" });
app.get("/checkout", async (req, res) => {
await flags.ready(); // resolves once, after the first download (or failure)
const f = flags.forUser({ id: req.user.id, plan: req.user.plan });
res.render("checkout", { newCheckout: f.isEnabled("new-checkout"), limits: f.getJson("limits", {}) });
});Next.js
Use @guidelytech/nextjs/server (getFlags(), getFlagsBootstrap()): it keeps one client across
requests and hot reloads and bypasses the Next.js fetch cache. Pass getFlagsBootstrap() to the
browser provider to render the same values on the server and the client.
Edge runtimes and serverless
Instances are short-lived, so don't poll — refresh on read once the cache is stale:
const flags = createFlagsClient({ sdkKey: "pk_prod_…", pollingMode: "lazy", cacheTtlSeconds: 30 });API
| | |
|---|---|
| ready() / isReady() | First load settled (bootstrap, network success or failure). Never rejects. |
| isEnabled(key, fallback = false) | Boolean flags only: any other value gives fallback. |
| getValue(key, fallback) | Value, or fallback when missing, null or of another JSON type. |
| getString / getNumber / getBoolean / getJson | Typed getters with the same rule. |
| getDetails(key, fallback) | Value plus reason, ruleId, variationId, experiment, usedFallback. |
| getAll() | Every flag's value (records no exposures). |
| forUser(attributes) | Getters for other attributes (merged over the client's). |
| setAttributes / updateAttributes / getAttributes | Replace / merge / read the attributes. |
| setOverrides(map \| null) | Local values (see overrideBehaviour). |
| setOffline(bool) / refresh() | Pause network activity / fetch now. |
| getPayload() | The payload (or remote results) to bootstrap a browser from server rendering. |
| subscribe(listener) | Change notifications; returns unsubscribe (works with useSyncExternalStore). |
| flush() / destroy() | Send usage now / stop timers and streams. |
Getters never throw and never block: before flags load (or if Guidely is unreachable) they return your fallback, and afterwards the last good payload.
Options
| Option | Default | |
|---|---|---|
| sdkKey (alias projectId) | — | pk_… or gly_sk_…. |
| apiUrl | https://guidelytech.com | Your Guidely URL. |
| environment | — | Required with gly_sk_ keys. |
| attributes | {} | Default attributes. |
| pollingMode | auto | auto (every pollIntervalSeconds, default 60), lazy (on read after cacheTtlSeconds), manual (refresh() only), stream (server-sent events, polling while disconnected). |
| payload / remoteResults | — | Bootstrap: used immediately (ready at once). |
| remoteEval | false | Evaluate on the server (POST /flags/evaluate) for environments that hide their rules. Switched on automatically when a public key gets remote_evaluation_required. Re-evaluates (debounced) when attributes change; results are cached per attribute set. |
| decryptionKey | — | For encrypted payloads (AES-256-GCM via WebCrypto). |
| overrides / overrideBehaviour | — / localOverRemote | localOnly (never fetch; overrides, then the bootstrap payload), localOverRemote (overrides win), remoteOverLocal (only for flags missing remotely). |
| devOverrides | true | Read dev overrides from environment variables and test globals. |
| enableUrlOverrides | false | Browser QA: ?guidely_flags=new-checkout:true,checkout-button-color:"green". |
| stickyBucketService | — | InMemoryStickyBucketStore / LocalStorageStickyBucketStore: users keep their experiment variation. |
| storage | localStorage in browsers | Any getItem/setItem store, sync or async (AsyncStorage). false disables. |
| trackingCallback(experiment, result) | — | Once per (experiment, user, variation): send exposures to your analytics. |
| trackExposures | true unless trackingCallback | Send exposures to Guidely for experiment results. |
| reportUsage | true | Aggregated evaluation counts, at most every 60 s (sendBeacon on page hide). |
| onReady, onChange, onFlagEvaluated, onError | — | Hooks. |
| offline, fetch, logger, now | — | Start offline; custom fetch; null silences logs; clock for tests. |
createStaticFlags({ payload, attributes }) gives the same getters over a payload with no network
or tracking — framework bindings use it to server-render and hydrate.
Dev overrides (CI and tests)
Pin flag values without touching the app. Every client reads these once, when it is created:
| Source | Example |
|---|---|
| GUIDELY_FLAG_OVERRIDES | new-checkout:true,limit:5,theme:"dark" or {"new-checkout":true} |
| GUIDELY_FLAG_OVERRIDES_FILE | path to a file holding the same text (JSON object or one key:value per line) |
| GUIDELY_FLAG_OVERRIDE_BEHAVIOUR | localOnly — no network, no cached payload: hermetic tests |
| globalThis.__GUIDELY_FLAG_OVERRIDES__ / __GUIDELY_FLAG_OVERRIDE_BEHAVIOUR__ | an object or the same text, set by a test runner in the page |
GUIDELY_FLAG_OVERRIDES='new-checkout:true' GUIDELY_FLAG_OVERRIDE_BEHAVIOUR=localOnly pnpm test
GUIDELY_FLAG_OVERRIDES_FILE=ci/flags.json pnpm testA sample ci/flags.json (keys are flag keys; give each value the flag's type, since a value of the
wrong type or null makes the getter return its fallback):
{
"new-checkout": true,
"checkout-button-color": "green",
"max-cart-items": 25,
"pricing-page": { "layout": "grid", "plans": ["starter", "pro", "team"] },
"holiday-banner": null
}// Playwright: set before the page's scripts run.
await page.addInitScript(() => {
(window as any).__GUIDELY_FLAG_OVERRIDES__ = { "new-checkout": true };
});
// Cypress: cy.visit("/", { onBeforeLoad: (win) => { win.__GUIDELY_FLAG_OVERRIDES__ = { "new-checkout": true }; } });Per flag, later wins: overrides / setOverrides() → file → variable → global → ?guidely_flags=.
setOverrides() only replaces the first. Values are served with reason OVERRIDE. The client
logs one warning listing the sources in use, so a variable left set in production shows up in
logs; malformed sources go to onError and are ignored. devOverrides: false turns it all off.
With server rendering, set both: the variable for server code and the global for the browser
(React and Svelte server renders read the bootstrap payload, which carries no overrides).
parseOverrides(text) is exported for reading the same format elsewhere.
CLI
# In CI on your main branch: upload where each flag is used in code (shown on the flag's page).
npx guidely-flags code-refs --key "$GUIDELY_API_KEY" --environment production --repo acme/web
# Debug an evaluation.
npx guidely-flags evaluate --key pk_prod_… --flag new-checkout --attributes '{"id":"u1","plan":"pro"}'code-refs needs a gly_sk_ key with flags:read and flags:code-refs. It scans files from
git ls-files (or a walk skipping node_modules, dist, build, vendor…) under 1 MB for flag
keys written as quoted string literals, and uploads { repo, branch, commit, refs }. Branch and
commit default to the current git checkout (or GITHUB_REF_NAME / GITHUB_SHA). --dry-run
prints the references instead. guidely-flags --help lists every option.
The evaluation semantics are specified in docs/feature-flags-spec.md.
