Flags
Feature flags in the browser and on the server, decided by PostHog through the Flags SDK.
@zap-ts/flags gives the app feature flags. A feature flag is a switch. You
turn a feature on for some people, and off for the rest, without shipping new
code.
The package has two export paths. @zap-ts/flags/client gives you four React
hooks, to read a flag inside a component. @zap-ts/flags/server gives you an
Effect service, to read a flag during a request.
PostHog decides the answer in both cases. The package talks to it through
@flags-sdk/posthog, the PostHog adapter of the Flags SDK.
How it relates to the analytics package
Flags and analytics are two packages on one PostHog project. They share
POSTHOG_KEY and POSTHOG_HOST. They share nothing else. Neither one depends
on the other.
But they do meet in the browser. The four hooks come straight from
@posthog/react. They read PostHog from React context. So
AnalyticsProvider from @zap-ts/analytics/client must
sit above them in the React tree. The same provider that names the user for
events also lets PostHog decide a flag for that user. Remove the provider, and
the hooks have nobody to ask.
On the server there is no such link. @zap-ts/flags/server builds its own
PostHog adapter from the same two variables. It does not need
ServerAnalyticsLive.
One simple way to remember the difference: analytics sends facts out, and flags read decisions in.
In a component
import { useFeatureFlagEnabled } from "@zap-ts/flags/client";
const Dashboard = () => {
const isNew = useFeatureFlagEnabled("new-dashboard");
return isNew ? <NewDashboard /> : <OldDashboard />;
};
There are four hooks:
useFeatureFlagEnabledgives youtrueorfalse.useFeatureFlagVariantKeygives you the name of the variant. A flag can have more than two answers, and each one is a variant.useFeatureFlagPayloadgives you the extra data attached to the flag.useFeatureFlagResultgives you the whole result.
The package re-exports these hooks without changing them. So they work exactly
as PostHog describes them. That includes one point to watch: a hook returns
undefined while the flags are still loading. Treat that as “not on yet”. Do
not treat it as a real answer.
On the server
Flags is a tag. A tag is a name that stands for a service you provide later.
This one holds the PostHog adapter. FlagsLive is the layer that builds it,
with createPostHogAdapter. A layer is the recipe that makes a service real.
import { evaluateFlag, FlagsLive } from "@zap-ts/flags/server";
import { Effect } from "effect";
const program = evaluateFlag({
key: "new-dashboard",
defaultValue: false,
entities: { distinctId: user.id },
request,
});
const enabled = await Effect.runPromise(program.pipe(Effect.provide(FlagsLive)));
evaluateFlag takes four things:
keyis the name of the flag in PostHog.defaultValueis the answer to use when PostHog gives none back. It also sets the type of the result.entitiessays who the flag is for. Put the user id in itsdistinctIdfield. PostHog cannot decide a flag until it knows who is asking.requestis the request of the current server function. The package reads its cookies and headers, and passes them to the adapter.
The Flags SDK has a flag() helper, but only for Next.js and SvelteKit. This
app uses neither. So the package calls the adapter itself, and reads the
cookies and headers from the request. That is why you pass the request in.
Does the call to PostHog fail? Then you get a FlagsError. It carries the
original error in its cause field. So an outage is a value your code can
handle, not a crash. Note the difference: an empty answer falls back to
defaultValue, but a failed call does not.
Every flag is decided by PostHog
The adapter asks PostHog every time. One decision is one call over the network.
That choice is on purpose, and packages/flags/src/server.ts says why. PostHog
can also copy the flag rules into memory and decide there. But that copy needs
a job that keeps asking PostHog for changes. That is a good trade on a server
that runs for days. It is a bad one on compute that starts and stops per
request, such as Cloudflare Workers. There, each new instance would start its
own job instead of sharing one.
Do you move the app to a long-running server, and do you read flags very often? Then this is the choice to look at again.
Environment
POSTHOG_KEY=
POSTHOG_HOST=https://eu.i.posthog.com
These are the same two variables the analytics package uses. The root
.env.schema file declares them once.
POSTHOG_KEY must start with phc_. The server layer reads it with
requireEnv. So if PostHog is not set up, the error comes when you read a
flag. POSTHOG_HOST is required, and points at the EU cloud by default.
See Environment to learn how varlock turns those
declarations into the typed ENV that the code imports.
Related
- Analytics — the provider the client hooks need
- Environment —
envandrequireEnv