Playground¶
The playground/ directory in the kiln repository is a runnable
multi-app FastAPI project that demonstrates kiln’s project mode. It
generates a blog API and an inventory API from Jsonnet configs, mounts
them both under /v1/, and serves them with uvicorn.
It is the fastest way to see kiln end-to-end without setting up your own project.
Structure¶
playground/
├── examples/
│ ├── project.jsonnet # top-level project config
│ ├── blog.jsonnet # blog app config
│ └── inventory.jsonnet # inventory app config
├── blog/
│ └── models/ # hand-written SQLAlchemy models
├── inventory/
│ └── models.py # hand-written SQLAlchemy models
├── db/
│ └── base.py # shared PGCraftBase
├── alembic/ # Alembic migration env
├── _generated/ # written by foundry generate (git-ignored)
├── main.py # FastAPI entry point
└── justfile # convenience recipes
_generated/ is the output directory. Everything inside it is
overwritten on every foundry generate run and should not be edited
by hand.
The blog/ and inventory/ directories contain hand-written code
that coexists with the generated _generated/blog/ and
_generated/inventory/ packages via Python
namespace packages — no
__init__.py in blog/ or inventory/, both parent directories on
sys.path.
Quick start¶
From the repo root, install all dependency groups:
uv sync --all-groups
Then from playground/:
just g # generate from examples/project.jsonnet
just s # start uvicorn with --reload
Or in one step:
just f # generate then serve
The server starts at http://localhost:8000. Interactive docs are
at http://localhost:8000/docs.
Justfile recipes¶
All recipes are run from the playground/ directory.
Recipe |
Alias |
Description |
|---|---|---|
|
|
Run |
|
|
Start uvicorn with hot-reload. |
|
|
Generate then serve. |
|
Apply all pending Alembic migrations. |
|
|
Generate a new autogenerated migration revision. |
Project config¶
examples/project.jsonnet is a project-level config — it declares
shared auth and databases, then lists each app with its URL prefix:
local auth = import "kiln/auth/jwt.libsonnet";
local db = import "kiln/db/databases.libsonnet";
{
auth: auth.jwt({ secret_env: "JWT_SECRET" }),
databases: [
db.postgres("primary", { default: true }),
db.postgres("analytics", { url_env: "ANALYTICS_DATABASE_URL" }),
],
apps: [
{ config: import "blog.jsonnet", prefix: "/blog" },
{ config: import "inventory.jsonnet", prefix: "/inventory" },
],
}
Each app config (blog.jsonnet, inventory.jsonnet) is a
self-contained kiln config with its own module, resources.
Kiln merges the project-level auth and databases
into each app config before generating, so app configs do not need to
redeclare them.
Apps¶
- blog (
module: "blog") Demonstrates plain
read_onlyandfullpresets alongside a custom CRUD resource. TheAuthormodel is read-only;Articlehas full CRUD with per-operation auth and two actions;Taguses thefullpreset with an integer PK and no auth.- inventory (
module: "inventory") Demonstrates the full range of resource patterns:
Product— full CRUD with a customroute_prefix, explicit field lists, per-operation auth, and two actions (one with params and auth, one with no params and no auth).StockMovement— create-only (append-only) record with adatefield.EventLog— thewrite_onlypreset (create/update/delete, no reads) backed by aPGCraftAppendOnlymodel on the analytics database. Exercisesdb_key,route_prefix,email/json/datetimefield types,require_auth: true(all operations), and a no-param no-auth action.
EventLog and PGCraftAppendOnly¶
inventory/models.py defines EventLog using
__pgcraft__ = {"factory": PGCraftAppendOnly}. This instructs
pgcraft to create an SCD Type 2 structure: a root table (for the
stable id/created_at identity) and an attributes table (for
mutable columns), joined through an event_logs view backed by
INSTEAD OF triggers. Each write appends a new attributes row,
preserving the full change history.
class EventLog(Base):
__tablename__ = "event_logs"
__table_args__ = {"schema": "public"}
__pgcraft__ = {"factory": PGCraftAppendOnly}
# Raw Column instances required — pgcraft's plugin pipeline
# collects columns from the class dict before ORM mapping.
event_type = Column(String(80), nullable=False)
actor_email = Column(String(254), nullable=False)
payload = Column(Text, nullable=True)
Kiln exposes it as a write_only resource on the analytics
database, so all mutations go to the secondary pool while reads
remain absent from the API entirely.