Coming from a Node backend

There is no long-lived process

Is Pithy for you? is the question above this one. What replaces the process is durable jobs and the Worker contract; what replaces the ORM is the data layer.

Your server does not start up, warm a cache, and stay running. A Worker is invoked per request and may be discarded between them.

So the pattern that breaks first is module-level mutable state:

// this does not do what it does in Node
const cache = new Map();

It may be there on the next request. It may not. And it is certainly not shared across the world.

For state that must persist, reach for KV, D1, or a Durable Object — which is the one thing in the platform that is a single addressable instance with its own storage.

No filesystem, and no node: most places

No fs. No local temp directory. No writing a file and reading it back.

Bytes go to R2. Structured data goes to D1. The kit’s own packages are written to this constraint — the ones that parse text or compute hashes reach for no node: import at all, so they run in a Worker as readily as in a build script.

No code generation at runtime

The runtime forbids it.

That is not a style rule — it is why the email capability’s templates are precompiled at build time and why its registry is closed to adopters. A template compiled where it runs is a template that cannot run.

CPU time is bounded, wall time is not the same thing

A request has a CPU budget, and a model call or a slow upstream does not fit inside it.

Which is why the kit’s pattern for anything slow is: persist first, dispatch a Workflow after. The support inbox stores the message and then classifies it — a model that is slow or briefly down must never take the persistence of somebody’s support request with it.

setInterval and setTimeout are not your friends here

In a Durable Object, a single setInterval prevents hibernation entirely — pinning the object in memory and billing duration continuously.

Use an alarm. In a Worker, use a cron.

Your ORM probably does not run

The kit uses Kysely, which is a query builder rather than a runtime with a driver stack.

D1 has no interactive transactions, so BEGIN/COMMIT around application logic is not available. The kit’s answer is to push correctness into the statement — a conditional INSERT … SELECT … WHERE that evaluates its own precondition, a CHECK constraint that a race cannot walk past, a DELETE … RETURNING that exactly one concurrent caller wins.

D1 also caps a query at 100 bound parameters. A generated IN (…) over a user-supplied list will meet it.

Middleware you already know

Hono is the router, and it behaves the way you expect. app.use, app.get, middleware that calls next().

What changes is where your routes live: in your app capability, composed last, so your middleware runs after auth has resolved the session and before another capability’s own gate.

What you gain on day one

No CORS, because your front end and API share an origin.

No connection pool, because D1 is reached over a binding rather than a socket.

No idle bill, because nothing is running when nothing is happening.

No deploy target to size. pithy deploy ships every Worker under apps/.

What to check before committing

It is Cloudflare-only. There is no Postgres adapter and there will not be one. If your backend needs to run somewhere else, this is the wrong kit — is Pithy for you? says so at greater length.

ESC