Projects and resources
the model
A project is a namespace. It holds resources, each of which has a kind. That is the whole model, and it is deliberately small: the shape is what lets later work add capabilities without rewriting earlier ones.
project "blog" primary postgres a database site app a Dockerfile, served over HTTP api worker a Worker on workerd jobs queue messages, with api bound as consumerThe four kinds
Section titled “The four kinds”packages/core/src/types.ts defines ResourceKind as
'postgres' | 'app' | 'worker' | 'queue'.
| Kind | Holds | Wakes on |
|---|---|---|
postgres |
A plain Postgres data directory | An inbound connection |
app |
Nothing. Stateless by design | An inbound HTTP request |
worker |
A built bundle, KV/R2/D1 state, and Durable Object storage | An inbound HTTP request |
queue |
Messages, in a sqlite file the daemon owns | A message arriving |
Durable Objects are a capability of worker,
not a fifth kind.
app is stateless on purpose. Volumes are Phase 3, which keeps volume lifecycle
out of the phase that was already the hardest.
Targets
Section titled “Targets”Most commands take a <target>:
blogwhen the project holds exactly one resource.blog/primaryotherwise.
The short form exists because the common case is one database in one project, and making that case type a slash is the kind of friction the project exists to remove.
States
Section titled “States”ResourceState is a small union, and the two that matter to a reader are
resting states rather than transitions:
| State | Resting | Means |
|---|---|---|
running |
yes | Up, and serving |
sleeping |
yes | Stopped. This is the product working, not a fault |
undeployed |
yes | The row exists, with an id and a hostname, and no code has been deployed to it |
creating, starting, stopping, destroying |
no | In transition |
failed |
yes | Something went wrong and was recorded rather than swallowed |
undeployed is the youngest of these and exists because creating a resource and
deploying code to it are two acts, not one. Before that split, the daemon
refused to write a resource row without a build source, which meant Studio and
MCP could not create an app at all, having no filesystem path to offer.
ADR 0014.
Where things live on disk
Section titled “Where things live on disk”Everything is under ~/.hobby, or $HOBBY_HOME if you set it.
~/.hobby/ state.db the daemon's own record hobby.sock the unix socket the CLI and MCP talk to hobby.json config projects/ blog/ primary/ pgdata/18/docker a plain PGDATA api/ bundle/ the built worker and its manifest state/ KV, R2, D1, cache do/ Durable Object sqlite files jobs/ queue/messages.sqliteAn app has no directory of its own, because it holds nothing.
The nesting under pgdata is not decoration. Postgres 18’s official image
refuses to start when a bind mount lands directly on what used to be PGDATA,
so the mount point is the postgres home directory and the entrypoint puts the
real data directory in a subdirectory named after the major version.
resolvePgdataPath in packages/core/src/config.ts is the one place that is
written down, and everything that needs the true on-disk path derives it from
there.
Adding a kind
Section titled “Adding a kind”Implement ResourceKindHandler (packages/core/src/kinds.ts) and add one line
to createDefaultKindRegistry. Four kinds already exist to copy from. That
seam, rather than a plugin system, is what
ADR 0007 means when it says the
phases are additive rather than successive rewrites.