pithy add turnstile --with-prerequisites--with-prerequisites composes secrets, which turnstile requires — the widget secrets are read through the registry.
What lands in your repo
apps/<worker>/pithy.config.ts gains the registration, and that is the whole diff:
turnstile({
widgets: { visible: {} },
}),wrangler.jsonc gains nothing. This capability declares no required bindings. There is no database, no bucket, no namespace and no Workflow — nothing durable to bind.
No migrations run, because there are no tables. pithy add turnstile is the shortest add in the kit.
The one thing you configure
widgets declares which widget modes exist: visible, invisible, or both.
turnstile({
widgets: {
visible: {},
invisible: {},
},
}),That declaration is what pithy turnstile provision reads to know how many production widgets to create, and what the middleware’s mode option selects between when you run both.
There are no CLI flags for it, deliberately. A flag would be a second source of truth frozen at scaffold time, and it would drift the moment somebody enables a mode in config. The scaffolded front-end screens read the composed config at runtime for the same reason.
Then provision
pithy turnstile provisionThat does three things per declared mode: dev and staging get Cloudflare’s documented test secret, and production gets a real widget bound to your Worker’s production hostname, with its secret in the production store and its public sitekey in the production Worker vars.
There is no --env. A widget binds to the domain a human loads it on, so production is not a parameter — the hostname is resolved from the target Worker’s domains declaration, its route, or its BASE_URL. A Worker with none of the three is refused, naming the declaration to add.
A re-run reuses an existing production widget rather than creating a second one. Cloudflare never returns an existing widget’s secret, so it cannot be recomposed: a re-run leaves the stored secret exactly as it was and reports that it did. If the secret was never stored, re-running will not heal it — deprovision then provision is what does.
That is also the repair for a production gate answering turnstile/config:
pithy turnstile deprovision
pithy turnstile provisionpithy turnstile is the whole command surface.
Gate your first route
One import, one line:
import { turnstile } from "@pithy-sh/turnstile/src/http/middleware";
app.use("/signup", turnstile());Pass action where the route has a meaningful name — it binds the token to that route, so one solved elsewhere cannot be replayed against it. Pass mode when you run both widgets.
If your handler reads the raw request stream, use header-token mode — turnstile({ header: "x-turnstile-token" }) — so the gate never touches the body.
And if auth is composed, you are already done
@pithy-sh/auth auto-gates its own magic-link and one-time-code send routes the moment turnstile is present. No wiring, no config, no route line to add. Those are the two routes a bot most wants, and they are covered by composition alone.
Check it worked
pithy doctor reports turnstile under the Worker’s health. In dev, a request without a token gets turnstile/missing_token and one with a test token passes — which is the fastest way to confirm the middleware is where you think it is.