The Default Vocabulary
One word, two spellings
The beginner pages say block; the API says Component. They are the
same thing: a block is what you decorate, and after start() it sits on the
shelf as a Component record in framework.registry — identifier, facets,
and the object itself. Everything a block "is" lives on that record; see
Name Tags & the Registry for the mapping and
the API reference for the record's fields.
SPOC lets you invent any kinds you like — that's the whole point. But an ecosystem needs a shared language: a reusable app published by someone else can only declare components of kinds your project happens to have. So SPOC blesses one default vocabulary — five kinds with agreed meanings.
The rule, up front: deviate freely. Nothing enforces these names. The default is
what the starter template emits, what these docs teach, and what reusable third-party
apps may assume. Think of it like Django's models.py: powerful because everyone
means the same thing by it — except here it's a convention, not a law.
The five kinds
| Kind | A component is… | Lifecycle role |
|---|---|---|
models |
a domain data declaration (a class, a schema) | none — purely declarative |
views |
a callable a surface exposes (a route, a page) | none — a surface projects it |
commands |
a callable a project CLI exposes | none — the CLI projects it |
resources |
a live process-wide object (a pool, a client) | opened by on_startup, closed by on_shutdown |
hooks |
a callable a surface fires at named moments | none — your code dispatches it |
Four of the five are purely declarative: SPOC registers them, and a surface — the web binding, the CLI, the worker loop you build — enumerates the registry and exposes them. That's the projection pattern from Name Tags & the Registry.
resources is the interesting one.
Resources: live objects with a lifecycle
Every application has a few objects that must be opened once, shared everywhere,
and closed on the way out — a database pool, an HTTP client, a cache connection.
Most frameworks hand you an untyped bag for these (app.ctx.db, app.state). SPOC
already has something better: the registry.
The recipe is three small pieces, all public API you've already met: declare the
kind with on_startup/on_shutdown hooks (the only kind in the vocabulary that
uses them), register each resource as an instance that knows how to open() and
close() itself, and resolve it through the registry at call time. The complete,
runnable project is Add a Database — this page keeps
the reasons.
On start(), the kind's on_startup opens every declared resource before your
surface takes traffic; on shutdown(), on_shutdown closes them in reverse module
order. And because shutdown replaces the registry, resolving a resource after
teardown fails with a named error — you can never be handed a dead pool.
Three fine points:
- Resolve at call time, not import time. A module that grabs a resource at import
runs before
start()and gets nothing. Inside a view/command/hook body, the resource is always live. - If a module's own
initialize()needs a resource, declare the order:spoc.KindSpec("models", depends_on=("resources",)). That works across apps too — a kind is a phase, so every app'sresourcesis up before any app'smodels. What you cannot order that way is two apps' modules of the same kind; for those, the[spoc.apps]list decides, or resolve lazily. - Async projects declare coroutine
open/closehooks and boot withastart()/ashutdown()— same recipe, awaited. See Start & Stop.
Hooks: events without an event system
A hooks component is a callable; dispatching it is your surface's job, not
SPOC's — the kernel describes, it never executes. The pattern is one loop:
for record in framework.registry.by_kind("hooks"):
record.object(event) # your surface decides when, and with what
Name hook components after the moment they answer (on_order_created,
before_request) and every app can contribute to the same moments.
Where to see it running
The storefront example exercises the resource recipe end to end —
a resource opened at boot, resolved from another module mid-call, and closed at
shutdown, with the test suite watching both halves. The starter template
(spoc init myproject --template starter) generates the full vocabulary wired and
running.
Next: Plugins.