The dashboard is a Pithy application, built on the kit it administers. That is not a gesture — it makes us the first consumer of our own seams, and every friction we hit is filed as a bug in the kit rather than worked around.
This page is the record of standing it up: the commands in order, what each one left behind, and what did not work.
No product code appears here. What appears is the command sequence, the config shapes, where the secrets went, and the honest summary — so that you can start your own project the same way.
The eight commands
What the dashboard is is the overview; the quickstart is the same sequence for your own project; contributing is how to work on the kit itself.
| Command | Outcome |
|---|---|
pithy init | Scaffolded, in a repository already cloned |
bun install | 195 packages. Nothing 404s |
pithy ui add react | Ten files, and the asset allowlist derived from the real route table |
pithy add <capability> ×5 | Each left a project that typechecks |
pithy migrate | Six migrations, composed in order |
pithy seed | Idempotent, and structurally unable to touch production |
pithy dev | The app correctly refuses to serve without provisioned bindings |
pithy deploy | Needs an account. Verified up to that boundary |
1. Scaffold
pithy init --name dash --worker boardRoot package.json, pithy.config.ts, both vitest configs, biome.jsonc with its two lint plugins, .gitignore — and apps/board/ with its own config, wrangler.jsonc, pithy.worker.jsonc and mount file.
It ran in a repository already cloned, which is how projects normally start. It refuses only on collision with a path it would write.
2. Add the front end
pithy ui add react --worker boardTen files, plus rewiring pithy.worker.jsonc and package.json. The part worth having is that it derives the asset allowlist from the real composed route table — get that list wrong and every API route silently returns the SPA shell, with a 200 and no error anywhere.
One thing to set expectations on: the React package is not a component library. It ships zero runtime exports — no button, no table, no layout. It is a template copier. The design system is the adopter’s to write, and in this repository it is.
3. Compose the capabilities
pithy add secrets --worker board
pithy add email --worker board
pithy add audit --worker board
pithy add auth --worker board
pithy add payments --worker boardEach installs the package, reads its manifest, wires that Worker’s config and bindings, scaffolds its config options, and runs that Worker’s dev migrations.
Order does not matter. Capabilities carry a fixed migration order, and the ledger composes in that order regardless of the order you add them in.
Every capability leaves a project that typechecks. A required option the CLI cannot invent — a secret registry, a ledger’s currencies — is scaffolded as a working example with a comment saying to replace it, so the project compiles and loads before you have written a line.
4. Where the secrets went
This is the part most worth understanding, because it is not obvious from any single command.
~/.config/pithy/
cloudflare.json the CLI's own credentials. Account-scoped, shared by every project
dash/
secrets.jsonc every secret this project has ← the source
dev.json the dev-login preference
<repo>/apps/board/
.dev.vars generated. Never edit it
.dev.vars.local hand-authored overrides, merged last, winsNothing the CLI reads lives in the checkout. Not an application secret, and not a credential either.
The credentials are account-scoped rather than project-scoped: one Cloudflare account holds many projects, so a per-project copy would store the same token repeatedly and make rotation an N-place edit. They are also the strongest credential in the system — the token every other token is minted from — and a maintainer’s live token reaching a published npm tarball from a file inside a repo is a real, filed incident. npm pack does not consult .gitignore when the package declares a file list, so gitignoring was never the protection it looked like.
The secrets file is the source, not a copy. That was a real defect once: a value was copied into a second file by the seeding step and the generator read that, so editing the secrets file changed nothing until something re-seeded — and this project had two secrets diverged between the two files, with the Worker running on the copy. Generation reads the source directly now.
5. Migrate
pithy migrateSECRETS 0100_secrets_0001_init Success
DB 0200_email_0001_init Success
DB 0250_audit_0001_init Success
DB 0300_auth_0001_init Success
DB 1000_payments_0001_purchases Success
DB 2000_app_0001_init Success
EMAIL_SUPPRESSIONS 0100_email_0001_suppressions SuccessOur own migration sits at 2000 — core capabilities low, the adopter’s app high — so it runs after every table it references exists.
6. Seed, and run
pithy seed
pithy devSeeding mints a dev session from the preference file, so pithy dev starts signed in rather than making you round-trip a magic link. It is idempotent by comparison rather than convergence: running it again rewrites no bytes and re-mints nothing.
Dev secrets can never reach a managed environment. Not by flag, not by argument — the refusal is structural, because no signature in that path accepts one.
Cold start is slow: Vite takes around 28 seconds before the first request is served.
The most instructive output of the whole exercise
GET /health returned 500 until one real Cloudflare resource existed:
{"error":{"code":"core/internal","message":"Missing required bindings: workflow:EMAIL_SENDER"}}The master key was not in that list, and the difference is the thing to understand. It is a registry secret, so a dev value was minted locally and reaches the Worker through the generated file — local dev needs no account for it. The email Workflow binding points at a deployed Worker, and there is no local stand-in for one.
That is fail-fast binding validation working as designed: a composed capability declares what it needs, and the Worker refuses on the first request naming what is absent rather than failing later somewhere unrelated. The static client served regardless, so every pane was reachable.
7. Deploy, up to the account boundary
Not run — it needs an account, an API token and an active zone. What was verified is everything before that:
bun run build
bunx wrangler deploy --dry-run --outdir /tmp/outEleven bindings resolve and wrangler accepts the config.
One subtlety CI hit: the build writes a deploy-config redirect for a single already-resolved environment, so a dry run with a different environment flag after a dev build reads dev’s bindings and ignores the flag without saying so. The environment is chosen at build time, and CI does the dry run once per environment.
What still needs a real account
| Needs | For |
|---|---|
| An account, Workers Paid, a bootstrap token | Every deploy, every remote migrate |
| An active zone | The public hostnames |
| A Secrets Store | Recording the store id. One per account, so nothing to choose |
pithy secrets provision | The master key in deployed environments |
pithy email provision | The send Workflow, and a verified sending domain |
| A payment rail’s keys and webhook secret | Self-billing |
| A real project composing the control-plane seam | Every read pane |
The honest summary
All eight commands work today. That was not true when this document was first written. Three of them failed, a fourth left the project in a state where a later unrelated command failed, and this file was a list of defects with issue numbers beside them.
Ninety-three gaps were filed while building on the kit; eighty-four are closed. Every one was found by hitting it — by scaffolding a real application, running the real commands, and reading the real output. Several were found only by running a command twice, or by deleting the checkout and looking, or by planting a symlink where a tool did not expect one.
Three lessons came out of it that are worth more than the fixes.
Distrust the report. Roughly a third of confident findings did not survive contact with the code: an issue blamed the wrong file, a claim about which directory a test walked turned out false when measured, a suspected second defect simply was not there. Every fix in the later rounds began by reproducing the issue’s own claims before acting on them, and a surprising amount of the value came from that.
Defects come in families. Four classes had three or more producers each, always because a rule lived at a call site rather than at the thing being called — a file read whose failure was discarded, a string interpolated into generated source, a path joined without validation, a private directory walk. Reliable enough to be a search strategy: fix one, then go looking for the second and third, because they are there.
Workarounds are the measure. This repository carried three scripts that existed only to route around the CLI. Two are gone — capability installs work against a linked checkout, and each Worker’s local variables are generated. One stays until the packages publish, and it exists only because they have not.
That ratio is the argument for building the commercial product on the open-source kit. Nobody else would have found these before an adopter did — and an adopter would have found them alone, in the dark, with no issue tracker to file them in.