ShipKit docs
The stack

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/servergetFlag() / isFlagEnabled() for Bun + Node code (api, workers).
  • @workspace/feature-flags/clientuseFlag() / 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.betaFF_WEBHOOKS_BETA.

# .env.local
FF_WEBHOOKS_BETA=true
NEXT_PUBLIC_FF_WEBHOOKS_BETA=true

This 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

  1. Create the flag in PostHog (or set FF_<KEY> in your env for dev).
  2. Add the key to PUBLIC_FLAGS in apps/api/src/modules/v1/me/service.ts if it should be in the hydration payload.
  3. Use useFlagEnabled("…") in the UI or isFlagEnabled("…", { distinctId }) on the server.

That's it — no schema changes, no migration, no extra moving parts.

On this page