pithy add auth --with-prerequisitesThe flag is not optional advice. Auth declares two peer capabilities — secrets and email — and createBackend refuses to assemble a Worker missing either, naming the one that is absent. A project without them does not boot at all, so composing them is part of adding auth rather than a separate decision.
They compose deepest first: secrets, then email, then auth. At a terminal you would be asked once for the whole cascade; --with-prerequisites is that answer written down, and anything headless is refused with the exact commands in order.
What lands in your repo
apps/<worker>/pithy.config.ts gains an import and a registration call inside the managed region:
capabilities: [
// pithy:capabilities (managed region — do not remove this marker)
secrets(),
email({ fromAddress: "noreply@example.com", baseUrl: PUBLIC_ORIGIN }),
auth({ basePath: "/auth", baseURL: PUBLIC_ORIGIN }),
],PUBLIC_ORIGIN is written unquoted, not a literal. Every capability that needs a public origin asks the same question, and a URL typed into any one of them is one environment’s origin written into all of them — which is how a staging deploy mails real users magic links into production. pithy init scaffolds the derivation; add writes the constant.
apps/<worker>/wrangler.jsonc gains two bindings, in every environment stanza the file declares:
| Binding | Type | What it is |
|---|---|---|
DB | d1 | Where the pithy_auth_* tables live — the same database as your own tables |
AUTH_RATE_LIMITER | ratelimit | The edge guard in front of every auth route |
The rate limiter is a policy rather than a resource: nothing exists behind it in your account. It is written at 100 requests per 60 seconds, per client IP, which is a flood guard rather than a product rule. Tune it in wrangler.jsonc — add never rewrites an entry you have changed — and note that Cloudflare accepts a period of 10 or 60 and nothing else.
No handler code lands anywhere. The logic stays in the package and upgrades with it on a minor release. pithy add auth --eject is the escape hatch if the day comes that you want the source, and it is a one-way door: an ejected capability is never reconciled again.
What runs
add re-reads the config after wiring and runs that Worker’s dev migrations, so the migration that just arrived is in the registry. Auth’s tables are all pithy_auth_*:
users · sessions · accounts · verifications · jwks · rate_limit · devices
The first six are Better Auth’s. devices is Pithy’s own. Your own tables sit beside them, untouched.
What is minted, and where it goes
Two dev secrets land in <config>/<project>/secrets.jsonc — in the Pithy config directory, outside every checkout:
| Secret | Why |
|---|---|
auth-session-secret | Better Auth’s signing and encryption secret |
email-link-signing-key | From the email prerequisite, for tracking and unsubscribe links |
auth-session-secret is written only when absent. A new value signs out every live session, so re-running pithy add auth never replaces one.
Nothing here is in your repository. There is nothing to gitignore, nothing git add -A can reach, nothing npm pack can carry, and nothing an rm -rf on the clone destroys.
What it does not do
add reaches no Cloudflare account. It writes config and local D1 only, so everything below is left for later and reported in the run’s notes:
- Deployed environments need their own session secret.
pithy secrets create auth-session-secret— andpithy secrets provisioncreates it for you, because a session signing key is arbitrary: any random string works, since nothing outside the project validates one. - Email needs provisioning before a magic link is delivered for real.
pithy email provision. Until then,pithy devfalls back to the local simulator, says so in its ready banner, and writes the rendered message to disk — so the link is readable either way.
The three config options
| Option | Default | What it decides |
|---|---|---|
basePath | /auth | Where the handler mounts. It must match the OAuth redirect URIs you register, so changing it later means changing them too |
baseURL | PUBLIC_ORIGIN | The public origin of this Worker. Callbacks, JWKS and magic-link URLs are built from it. A dev composition ignores it and serves on whatever host the request arrived at — local dev has no TLS and its port is assigned per run, so it is the one address nobody can write down |
disableSignUp | false | When true, sign-in never provisions a new user. An unknown email gets no email at all, which is deliberate: replying no such account is an enumeration oracle |
You also set trustedOrigins by hand — every web origin and mobile deep-link scheme allowed as a redirect target and CSRF origin. Dev needs neither it nor baseURL: it resolves its own base URL and trusts its own origin.
Turning a social provider on
Each is a config flag plus one credential you take from that provider’s console. Nothing can mint them.
| Provider | Config | Secret | Console |
|---|---|---|---|
google: { enabled: true } | auth-google-credentials | Google Cloud credentials | |
| Apple | apple: { enabled: true } | auth-apple-credentials | Apple Developer keys |
| GitHub | github: { enabled: true } | auth-github-credentials | GitHub developer settings |
facebook: { enabled: true } | auth-facebook-credentials | Facebook apps |
Each is a typed JSON secret holding that provider’s client id and client secret together, created with pithy secrets create <name>. Each rotates manually — none of the four returns a replacement over an API, so a human opens the console and records the result.
Apple’s client secret is a signed JWT that expires within six months. Rotate it before it does.
There are no CLI flags for any of this. Which providers exist is declared in config, and a flag would be a second source of truth frozen at scaffold time — the scaffolded sign-in screens read the composed config at runtime, so enabling a provider stays a one-line edit and a redeploy.
Check it worked
pithy devThe Worker boots, and pithy doctor reports auth under the Worker’s health with its bindings satisfied and its migrations applied. If something is missing, it names the binding or the command rather than the symptom.