Five minutes, an empty directory, and no Cloudflare account. At the end of it a Worker is answering on localhost, and you will have seen every file you are expected to own.
You need the CLI installed and Node 22 or newer. Nothing else.
1. Scaffold
mkdir my-backend
cd my-backend
pithy initIt asks four things, and you can press enter through most of them.
The project name. It defaults to the directory’s name. This one is worth ten seconds of thought, because it leads every Cloudflare resource this project will ever provision — <project>-<env>-<thing> — and renaming it later does not rename them, it orphans them. It stops at 26 characters. One project, or two? is the scope question sitting behind it.
The first Worker’s name. Defaults to api, and it becomes the directory apps/api/. A project has as many Workers as it needs; another app is another Worker, not another project.
Which environments you deploy to. staging and prod is offered as the default, so one keypress is the whole answer. dev is never listed — it is local, it always exists, and it is never deployed.
Your Cloudflare credentials. Press enter to skip. You do not need an account for this page, and pithy doctor will name the missing credentials until you set them. If you do paste a token, init discovers which account it can see, and writes the credentials to the config directory outside every checkout — never into your repository.
2. Install
bun installSubstitute your package manager if it is not Bun — the scaffold declares no lockfile, so the first install is yours to choose.
3. Run it
pithy devThat starts every Worker in apps/, on ports pinned for this checkout, under one supervising process. It prints a labeled line per Worker and one ready banner when all of them have come up. The port for a single-Worker project is 8787.
4. Ask it if it is alive
In a second terminal:
curl http://localhost:8787/healthYou get {"status":"ok","version":…} — status is the liveness answer, and version is the deployed build id, which is null when the Worker cannot tell. GET /health is mounted by createEntrypoint and is public on purpose: it reads nothing about the caller, so there is no credential to send it.
That is the whole loop. Stop it with Ctrl + C, which tears down the Worker subtree with it.
What just got written
treeView-beta
my-backend/
pithy.config.ts ## project identity and policy — the name, the environments
package.json ## workspaces: apps/*
biome.jsonc ## the linter, configured
tsconfig.json ## a solution file
vitest.config.ts
vitest.workers.config.ts ## tests against real D1 and KV, through Miniflare
plugins/ ## two GritQL lint rules
apps/
api/
pithy.config.ts :::highlight ## what THIS Worker is made of
wrangler.jsonc ## wrangler's file — bindings, per environment
pithy.worker.jsonc ## yours — the dev manifest for this Worker
src/
index.ts :::highlight ## the mount file
bindings.workers.test.ts
Two of those files matter more than the rest.
apps/api/src/index.ts is the entire Worker.
import { createEntrypoint } from "@pithy-sh/core/src/createEntrypoint";
import config from "../pithy.config";
export default createEntrypoint(config);createEntrypoint assembles the capabilities named in the config into one Hono app: typed db and kv registries on every request, binding validation that fails fast with the binding’s name, and GET /health. It also fans inbound mail to any capability that handles it, which is how bounce processing works without you wiring anything.
apps/api/pithy.config.ts is what you own. Two lists and an app:
const config = {
domains: DOMAINS,
capabilities: [
// pithy:capabilities (managed region — do not remove this marker)
],
app,
};pithy add <capability> writes into that managed region. app is your own capability — routes, middleware, tables, bindings — and it composes last, after every library capability. There is genuinely nothing else. Handler code for auth, payments or storage never lands in your repository unless you ask for it with --eject.
Two things worth knowing now
Nothing here touched Cloudflare. pithy dev runs the Worker through Miniflare, the same local runtime wrangler dev uses, against local D1 and KV under .wrangler/state. Real resources arrive with pithy provision, and not before.
Ports are assigned, never probed. Each Worker’s port is pinned in this checkout’s .dev.config.json, out of a block reserved for this branch in a registry held once per machine. That is what lets several projects and several worktrees of each run at once. If something external has taken a pinned port, pithy dev reports the conflict and stops rather than quietly moving — a Worker that changes address breaks every sibling that was told where to find it.