Feature flags
Thin PostHog wrapper with an env fallback so dev and CI work offline.
Feature flags ship as a small wrapper around PostHog, with an env-variable fallback so dev / CI / preview branches keep working when POSTHOG_API_KEY isn't set.
The package lives at packages/feature-flags. Two entry points:
@workspace/feature-flags/server—getFlag()/isFlagEnabled()for Bun + Node code (api, workers).@workspace/feature-flags/client—useFlag()/useFlagEnabled()React hooks.
Server
import { getFlag, isFlagEnabled } from "@workspace/feature-flags/server";
const enabled = await isFlagEnabled("webhooks.beta", {
distinctId: user.id,
});getFlag() returns boolean | string (multivariate flags resolve to their string variant), isFlagEnabled() coerces to a strict boolean. Both swallow errors and return defaultValue (or false) — telemetry must never crash a request.
Decisions are cached for 30 seconds per (key, distinctId) so a hot path can call them freely. Use invalidateFlag(key) from tests or after admin edits if you need to bust the cache.
Client
"use client";
import { useFlagEnabled } from "@workspace/feature-flags/client";
export function WebhooksPage() {
const beta = useFlagEnabled("webhooks.beta");
return (
<header>
<h1>Webhooks</h1>
{beta && <span className="badge">Beta</span>}
</header>
);
}The hook reads from the already-initialised posthog-js instance (provided by <AnalyticsProvider> in apps/admin). Until PostHog has loaded — or when the user denied analytics consent — it falls back to process.env.NEXT_PUBLIC_FF_<KEY>.
Env fallback
The convention is FF_<KEY> server-side and NEXT_PUBLIC_FF_<KEY> client-side. Names get upper-snake-cased: webhooks.beta ↔ FF_WEBHOOKS_BETA.
# .env.local
FF_WEBHOOKS_BETA=true
NEXT_PUBLIC_FF_WEBHOOKS_BETA=trueThis is the recommended way to ship a flag that's "enabled in dev, controlled by PostHog in prod" — set the env var locally, leave it unset in production, and wire the rollout in PostHog UI.
Hydration
GET /api/v1/me/flags returns every flag in the PUBLIC_FLAGS allow-list (defined in apps/api/src/modules/v1/me/service.ts). The admin app can use this to hydrate flags on first render so the UI doesn't flicker between server-rendered "off" and PostHog-evaluated "on".
Adding a new flag
- Create the flag in PostHog (or set
FF_<KEY>in your env for dev). - Add the key to
PUBLIC_FLAGSinapps/api/src/modules/v1/me/service.tsif it should be in the hydration payload. - Use
useFlagEnabled("…")in the UI orisFlagEnabled("…", { distinctId })on the server.
That's it — no schema changes, no migration, no extra moving parts.