Apps & Modes
An app is a folder of related blocks — one file per kind. If your
framework declares models and views, an app looks like this:
apps/blog/
├── __init__.py
├── models.py # blocks of kind "models"
└── views.py # blocks of kind "views"
Three simple rules:
- The folder name is the namespace. Blocks in
apps/blog/get tags likemodels:blog.post. - The file name is the kind. A
modelsblock lives inmodels.py— put it inviews.pyand SPOC refuses with a clear error. Where a block lives is what it is. - Importing is fine.
from .models import Postinsideviews.pyis just a use, not a second claim. SPOC knows the difference.
spoc app <name> generates this shape for you, with the kinds read from your
own framework.py.
Installing apps
An app exists when its folder is on disk; it's installed when a mode lists
it in spoc.toml:
Each entry is a normal dotted Python path, imported exactly as written — the last segment becomes the namespace. Modes cascade (development ⊃ staging ⊃ production), as covered in The Settings File.
Load order: depends_on
If views need models to exist first, say so once, in the declaration:
A kind is a phase, and a phase spans every app. All apps' models.py load
and initialize before any app's views.py — not just within one app. So a
views hook that reads the registry sees every model in the project, never a
half-built world. A dependency cycle is refused at boot, with the cycle named.
An optional kind (required=False) may be missing from any app — every other
kind's module must exist, so a forgotten file is an error, not a silent gap. An
app that omits one moves nothing: its remaining modules stay in their own
phases, exactly where they would be if it had the file.
Ordering two apps: the [spoc.apps] list
Inside one phase, apps go in the order they are listed. That is the whole knob, and it is worth knowing about for exactly one reason — hooks fire in load order:
Both apps' models hooks run before either app's views hook (that is the
phase rule above), and within the models phase apps.core runs before
apps.blog. Reorder the list and you reorder the hooks. Shutdown runs the
whole thing backwards.
There is deliberately no way to say "all of apps.core before any of
apps.blog" — that would put one app's views ahead of another's models and
break the phase guarantee everything else here rests on.
Talking across apps
Here's the part that keeps big projects clean: apps don't import each other. They meet at the registry.
From the storefront example that ships with SPOC — the orders app uses the
catalog app's blocks without ever importing apps.catalog. This is the
example's real file, included at build time, so the storefront's own test suite
runs exactly what you read here:
import dataclasses as dc
from framework import framework, view
from .models import Order
# Cross-namespace, the registry way: orders never imports catalog's modules.
# It resolves catalog's objects by canonical identifier at call time, so the
# only coupling between the two apps is the identifier grammar itself.
@view
def order_summary():
product_cls = framework.resolve("models:catalog.product").object
stock = framework.resolve("views:catalog.list_products").object()
order = Order(id=1, product_id=1, quantity=2)
product = next(
product_cls(**entry)
for entry in stock["products"]
if entry["id"] == order.product_id
)
return {
"order": dc.asdict(order),
"product": dc.asdict(product),
"total_cents": product.price_cents * order.quantity,
}
The only thing the two apps share is the name-tag grammar. Swap the catalog app for another one that registers the same tags, and orders never notices.
Next: start & stop — the lifecycle.