Getting started

This guide walks through setting up a new project with the be target from scratch. By the end you will have a working FastAPI application with generated routes, schemas, and (optionally) auth wired to SQLAlchemy models you define yourself.

For the fastest path – a fully scaffolded main.py / pyproject.toml / justfile / config/project.jsonnet skeleton – skip ahead to Fast path: bootstrap with be_root; the rest of this page walks through the underlying pieces by hand.

For the React / TypeScript side, see Usage – the fe and fe_root targets are introduced there.

What be generates – and doesn’t

The be target generates FastAPI code from a config file. Specifically, it produces:

  • Routes – one FastAPI router per resource, with handlers for the CRUD operations (and custom actions) you enable.

  • Pydantic schemas – request and response models for every route.

  • Serializers – model-to-schema helpers used by the generated handlers.

  • An app router – aggregates all resource routers into one APIRouter your FastAPI app can mount.

  • A project router (multi-app projects only) – mounts every app router under its configured prefix.

  • Scaffolding – database session factories (one per configured database) and, when auth is enabled, a get_current_user dependency and optional login router.

be does not generate your SQLAlchemy models. You write those yourself and point the config at them by dotted import path.

Prerequisites

  • Python 3.14+

  • uv (recommended) or pip

  • A PostgreSQL database, if you want to run the generated code

Install

pip install kiln-generator

# or with uv
uv add kiln-generator

# or, to make ``codegen`` available globally without adding a
# Python dep to your project:
uv tool install kiln-generator

Verify the CLI is available:

codegen --help
codegen targets list

kiln-generator ships four targets (be, be_root, fe, fe_root). When more than one is installed, every codegen command takes --target <name> to pick.

Fast path: bootstrap with be_root

The be_root target is a one-shot scaffolder that emits the boilerplate you’d otherwise type by hand: main.py, pyproject.toml, justfile, .gitignore, and the starter config/project.jsonnet. (requires-python in pyproject.toml is the only Python-version pin – no .python-version since uv reads it from there.) Write a small bootstrap.jsonnet describing the project’s identity and which optional bits to enable:

{
  name: "myapp",
  module: "myapp",
  description: "FastAPI backend bootstrapped by be_root.",
  opentelemetry: false,
  auth: true,
  psycopg: true,
  pgcraft: false,
  pgqueuer: false,
  editable: false,
  rate_limit: false,
  comms: false,
  notification_preferences: false,
}

Setting rate_limit: true adds the kiln-generator[rate-limit] extra and stamps a rate_limit: rate_limit.slowapi('...') block into config/project.jsonnet pointing at a placeholder bucket-model dotted path you fill in once your model exists.

Setting comms: true stamps a comms: comms.platform({...}) block and emits a starter comms.py skeleton with stub context schemas, a stub Transport, and a stub PreferenceResolver. comms requires pgqueuer: true – the bootstrap rejects the combination otherwise so the broken state is caught at config-load time. See Communication platform for the runtime surface.

Setting notification_preferences: true (requires comms: true) upgrades the comms scaffold: the stub PreferenceResolver is replaced by a real DbPreferenceResolver querying {module}.models.NotificationPreference, and the per-app config/{module}.jsonnet gains a full-CRUD resource for managing the preference rows. You still own the NotificationPreference SQLAlchemy class – subclass NotificationPreferenceMixin on your project’s Base and migrate the table.

Then run:

codegen generate --target be_root --config bootstrap.jsonnet --out .

Every output is if_exists="skip", so re-running after editing the bootstrap config is non-destructive. Pass --force (or --force-paths a,b,c) when you want a re-run to clobber.

After the bootstrap, jump to Step 3 – Generate – the Step 1 (SQLAlchemy models) and Step 2 (config) sections below describe what the bootstrap already gave you.

Project layout

By default be writes all generated code into _generated/ (controlled by the package_prefix config field). A typical single-app project looks like:

myproject/
├── config/
│   └── project.jsonnet    # be config
├── myapp/
│   └── models.py          # your hand-written SQLAlchemy models
├── main.py                # your FastAPI entry point
└── _generated/            # written by be (never edit)
    ├── auth/              # get_current_user dependency
    ├── db/                # async session factories
    └── myapp/
        ├── routes/        # FastAPI routers
        ├── schemas/       # Pydantic request/response models
        └── serializers/   # model-to-schema helpers

Everything under _generated/ is overwritten on every codegen generate --target be run. Source-control the config file and your models, not the generated output.

Step 1 – Define your SQLAlchemy models

be generates routes and schemas around SQLAlchemy models you define. A minimal myapp/models.py:

import uuid
from datetime import datetime

from sqlalchemy import DateTime, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import (
    DeclarativeBase, Mapped, mapped_column,
)


class Base(DeclarativeBase):
    pass


class Article(Base):
    __tablename__ = "articles"

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        primary_key=True,
        default=uuid.uuid4,
    )
    title: Mapped[str] = mapped_column(String)
    body: Mapped[str] = mapped_column(String)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
    )

Step 2 – Write a config

Create config/project.jsonnet at your project root:

{
  version: "1",
  module: "myapp",
  package_prefix: "_generated",

  databases: [
    { key: "primary", url_env: "DATABASE_URL", default: true },
  ],

  resources: [{
    model: "myapp.models.Article",
    pk: "id",
    pk_type: "uuid",
    route_prefix: "/articles",
    require_auth: false,
    operations: [
      "get", "list", "create", "update", "delete",
      {
        name: "create",
        fields: [
          { name: "title", type: "str" },
          { name: "body",  type: "str" },
        ],
      },
    ],
  }],
}

Key points:

  • model is the dotted import path to your SQLAlchemy class. be does not require a specific base class – any SQLAlchemy DeclarativeBase subclass works.

  • operations lists the operations to run for this resource. A string is shorthand for the operation with default options. An object with name carries per-operation options (here, specifying exactly which fields the CreateRequest schema should expose).

  • databases produces one async session factory per entry. Set default: true on exactly one; resources omitting db_key use the default.

Step 3 – Generate

Run the CLI:

codegen generate --target be --config config/project.jsonnet

Output lands in _generated/:

_generated/
├── db/
│   ├── __init__.py
│   └── primary_session.py
└── myapp/
    ├── __init__.py
    ├── routes/
    │   ├── __init__.py          # app router
    │   └── article.py
    ├── schemas/
    │   └── article.py
    └── serializers/
        └── article.py

--out overrides the output root; --clean runs codegen clean first to remove any stale files.

Step 4 – Mount the router

Wire the generated router into your FastAPI app:

from fastapi import FastAPI

from _generated.myapp.routes import router

app = FastAPI()
app.include_router(router, prefix="/v1")

Step 5 – Environment and database

The generated session factory reads the URL from an environment variable (url_env on the database config – default DATABASE_URL):

export DATABASE_URL="postgresql+asyncpg://user:pw@localhost/mydb"

Create the database tables with whatever migration tool you use (Alembic is a common choice). be does not manage schema migrations.

Step 6 – Run the server

uvicorn main:app --reload

Interactive API docs land at http://localhost:8000/docs.

Adding authentication

Add an auth block to the config to turn on JWT authentication:

{
  ...,
  auth: {
    type: "jwt",
    secret_env: "JWT_SECRET",
    algorithm: "HS256",
    verify_credentials_fn: "myapp.auth.verify_credentials",
  },
  resources: [{
    model: "myapp.models.Article",
    require_auth: true,  // applies to all handlers on this resource
    ...
  }],
}

You provide verify_credentials – be generates the rest (auth/dependencies.py with get_current_user, and auth/router.py with a /auth/token login endpoint).

Auth is implemented as a cross-cutting @operation with a when hook – it runs whenever both config.auth is set and resource.require_auth is true. No extra wiring is required on your end.

Multi-app projects

For projects that bundle multiple apps (a blog API and an inventory API, say), wrap each app’s config in an apps list:

// project.jsonnet
{
  version: "1",
  package_prefix: "_generated",
  auth: { type: "jwt", secret_env: "JWT_SECRET", ... },
  databases: [{ key: "primary", default: true }],

  apps: [
    { config: import "blog.jsonnet",      prefix: "/blog" },
    { config: import "inventory.jsonnet", prefix: "/inventory" },
  ],
}

codegen generate --target be --config project.jsonnet produces the per-app code plus a top-level _generated/routes/__init__.py that mounts each app at its prefix. Mount that in FastAPI:

from _generated.routes import router

app = FastAPI()
app.include_router(router, prefix="/v1")

See the Playground for a runnable multi-app example with auth, multiple databases, and custom actions.

Where to next

  • Usage – day-to-day usage patterns, the full be config shape, and an introduction to the fe / fe_root frontend targets.

  • Extending kiln – add your own operations, renderers, or targets.

  • Architecture – how the engine, scopes, operations, and renderers fit together.

  • Reference – the exhaustive config reference.