What is a capability?

A capability is a package that contributes a set of things to your Worker: routes, middleware, bindings, migrations, Workflows, English, and a slice of config.

core, each @pithy-sh/* package, and your own app are all the same kind of object. There is no special case for the thing you wrote, which is the property most of the rest of this page follows from.

One contract

The Worker contract is what assembles them, pithy.config.ts is where you compose them, and the catalog is what there is.

const app = defineCapability({
  name: "app",
  requiredBindings: [{ type: "d1", name: "DB" }],
  databases: { app: { binding: "DB", tables, migrationOrder: 2000, migrations } },
  routes: (a) => {
    a.get("/hello", requireAuth(), handler);
  },
});

That is the same call @pithy-sh/auth makes. Your app composes last, after every library capability, and it can contribute any subset of the same things.

createBackend assembles them into one Hono app: it merges every capability’s databases and KV namespaces into typed registries on the request context, validates bindings fail-fast, mounts every middleware and every route in composition order, and serves GET /health.

The name is five things at once

name is not a label. It is the identity every namespaced thing in the kit derives from:

It isExample
The pithy add <name> argumentpithy add auth
The migration namespace0300_auth_0001_init
The table prefixpithy_auth_users
The error-code domainauth/invalid_token
The first segment of every workflow dispatch keyemail/send

That is what makes one capability’s contributions incapable of colliding with another’s, rather than merely unlikely to. A capability may only declare messages under its own name, and the composer refuses anything else.

Your app’s name is its migration namespace too, which is why it is not something to change casually once tables exist.

The logic lives in the package

This is the design decision the whole kit is arranged around, and it is worth naming plainly: pithy add auth puts no handler code in your repository.

What lands is an import and a registration call. The routes, the token exchange, the session logic, the rate limiting — all of it stays in the package and upgrades when the package does. A minor release fixes a bug in your auth without you merging anything.

The alternative — scaffolding the source into your repo — makes every adopter a fork on the day they scaffold. A typo fix or a security patch then reaches nobody.

The cost is that you cannot edit it. pithy add <capability> --eject is the escape hatch: it copies the source into your repo and repoints the wiring at your copy. It is a one-way door, and pithy upgrade reports an ejected capability as skipped forever after.

Capabilities depend on seams, never on each other

A capability never imports a sibling package. It reads a seam in core.

requireAuth() is the clearest example. Capabilities across the kit gate routes with it, and none of them validates a token: they read the identity seam that @pithy-sh/auth fills. With auth absent, that seam is null and those routes deny — which is a defined behavior rather than a crash.

The same shape appears three more times:

SeamFilled byInert behavior
The translator@pithy-sh/i18nEnglish, from what each capability contributed
The audit recorder@pithy-sh/auditA no-op, so an audited action never breaks for want of auditing
The entitlement resolver@pithy-sh/paymentsDenies, so a gate with no provider fails closed

Present and inert, filled by composition. That is why composing a capability late needs no rewiring anywhere: every call site was already there, calling a seam.

Some dependencies are real, and they are enforced at boot

A manifest’s peer capabilities are not advice. createBackend refuses to assemble a capability whose peers are missing, naming the one that is absent — so a Worker missing one does not start at all.

They are a graph rather than a list, composed deepest first. pithy add auth --with-prerequisites walks it.

Capabilities are per Worker

Everything a capability drives is per Worker: the composed route tree, the bindings written into that Worker’s wrangler.jsonc, and Durable Object class migrations, which register a class against a specific script.

So apps/<name>/pithy.config.ts holds what that Worker is made of, and the root pithy.config.ts holds only what cannot be per-Worker — the project name, the environment list, account-level token profiles, and one seeding safety policy.

A KV-only Worker never sees a D1 it does not use.

What a capability owns, and what stays yours

Its own tables, prefixed pithy_<name>_, sitting in the same database as yours and joinable with plain SQL. The prefix exists so the kit’s tables can never clash with yours — and your tables carry no prefix, because you are the adopter.

Its own error domain. Every code is domain/reason, and the domain is the capability name.

Its own English. Contributed through the contract, merged by the composer, and the reason the translator seam works with nothing composed.

Its own migrations, with a migrationOrder that is unique within its database and stable forever — renumbering a released capability renames its composed keys, and the migrator then reads applied migrations as unapplied and runs them again.

When you are writing one

Your app already is one. Everything on this page applies to it: it contributes routes, it can declare tables, it has a migration namespace, and it composes through the same contract.

Writing a library capability — one meant to be added to somebody else’s Worker — is a bigger commitment, and the kit’s own standard is worth knowing before you start. A capability is done when it ships the package, its manifest, namespaced migrations with tested rollbacks, its routes with declared verification and declared request contracts, a schema per admin response, its stamped version, its pithy add wiring, tests, a security review, and docs.

Docs are part of the product rather than an afterthought.

ESC