Your front end needs to know whether payments exist. Whether social sign-in is on. Which providers. Whether there is a paywall to render.
The usual answers are all slightly wrong. An environment variable duplicated across two projects, drifting from the day it is written. A /config endpoint fetched at startup, adding a round trip before your first paint. Or a hardcoded boolean somebody flips by hand and forgets in staging.
The kit does something simpler:
import auth from "virtual:pithy/auth";Projected from the Worker’s config
There is one virtual module per capability, generated by a Vite plugin that reads the Worker’s own pithy.config.ts.
Whatever you composed on the server is what the client sees. Not a copy of it, not a second declaration of it — a projection of the same file, inlined at build time.
So there is nothing to keep in sync, because there is nothing duplicated. Add payments to your Worker and the front end’s virtual:pithy/payments starts describing a composed capability. Remove it and it stops. No env var, no fetch, no deploy-order problem where the client learns about a capability before the server has it.
A capability you do not have still imports
Here is the design decision I want to point at.
A capability the Worker does not compose still resolves. It gives you { enabled: false } rather than failing the import — and that holds for a capability name nobody has ever heard of.
The reason is written in the plugin, and it changes how screens get written:
a screen branches on
enabled, it does not guard on whether the module exists.
Those are very different jobs. Guarding on module existence means try/catch around an import, or conditional dynamic imports, or a build that breaks when a capability is removed. Branching on a boolean is an if.
It also means removing a capability from your Worker does not break your build. The screens that used it start rendering their disabled state, which is exactly what you want while you decide whether to delete them.
Client-safe is enforced, not intended
A projection crosses from your server config to a bundle that ships to browsers. So what may be in it is guarded rather than trusted.
Two details from the guard that show the care:
A key that vanishes is somebody’s bug, and the guard names whose. For a projection, that is the capability author — not you. An error that tells the wrong person to fix something is an error that gets ignored.
A silent value change is not a projection. NaN becoming null on the way through would be a value quietly changing meaning between server and client. The guard refuses it rather than serializing something the server did not say.
That is the right instinct for anything crossing a boundary: refuse what you cannot represent, rather than representing it approximately.
What it costs
Build-time inlining means the values are fixed at build. Change your Worker’s config and the front end needs rebuilding — which is correct, since the config is now different, but it does mean this is not a runtime feature-flag system and should not be used as one.
If you need to flip something without a deploy, that is a different mechanism, and it should be, because a runtime flag has a different failure model from a compile-time constant.
Why this is more than convenience
Because the alternative is not “slightly more work”. It is a second declaration of your backend’s shape, living in your front end, maintained by hand.
Every one of those I have seen drifts. Not immediately — it drifts the day somebody adds a capability under deadline and updates the server but not the client, and the failure lands in a screen that renders a sign-in button for a provider that is not configured.
Making the client’s knowledge a projection of the server’s config means that divergence is not a thing you have to remember to avoid. There is one config, and one derived view of it.