Design

How this application is put together: why the files, tasks, and configuration exist, and how the pieces work together during development and deployment. Based on DESIGN.md in the repository root.

The stack

The application is a standard FastAPI app, assembled with holm. Pages render on the server. The browser receives that HTML plus two built static assets: the stylesheet and the JS bundle.

holm

A Next.js-like metaframework: standard FastAPI, htmy, and HTMX composed through file-system conventions. Files in app/ become pages and actions.

FastAPI

The standard web framework underneath. Pages and actions are plain FastAPI routes, so the ecosystem works unchanged.

htmy

Server-side rendering in typed, async Python with JSX-like components. No template language required.

HTMX

Dynamic updates without a client framework. Endpoints return HTML fragments, rendered via FastHX.

TailwindCSS

Utility-first styling for the application and its components.

BasecoatUI

Accessible components as editable Python htmy code, vendored into your repo from htmui. You own the files.

New to holm? The holm in a hurry guide covers the essentials in under five minutes.

Directory layout

.
├── .agents/skills/      # holm-web agent skill
├── app/                 # Application package; holm walks this
│   ├── main.py          # FastAPI app, holm wiring, static files, layout slots
│   ├── settings.py      # pydantic-settings; which CSS/JS files to serve
│   ├── head.py          # <head> component, with page metadata support
│   ├── nav.py           # Root navigation; highlights the current page
│   ├── error.py         # Error handlers; 404 redirects to /not-found
│   ├── layout.py        # Root layout; renders layout.jinja (optional)
│   ├── layout.jinja     # Root layout markup
│   ├── not_found/       # Not-found page package (served at /not-found)
│   │   └── page.py
│   └── page.py          # Home page
├── assets/              # Stylesheet and JS bundle sources
│   ├── app.css          # Tailwind input; your CSS goes here
│   └── app.js           # JS entry point; your scripts go here
├── components/          # BasecoatUI as Python; yours to edit
├── static/              # Build outputs, served at /static
├── Procfile             # honcho process definitions (poe start)
├── package.json         # Tailwind, HTMX, and the JS bundler
└── pyproject.toml       # Python dependencies, tool config, poe tasks

This application also includes a greeting action (app/actions.py), this design page (app/design/), a component showcase (app/showcase/), and demo styles (assets/demo.css). Delete any you don't need.

Components live outside app/

holm walks app/: page.py becomes a route, layout.py or layout.jinja wraps pages, actions.py defines endpoints that return HTML fragments. UI components live in components/ at the project root, where discovery never touches them. Import them as from components.button import button.

assets/ vs static/

assets/ holds the source files for the stylesheet and the JS bundle; you edit those. static/ holds the compiled files the app serves. Don't edit anything in static/ by hand.

Agent support

The project ships the holm-web agent skill in .agents/skills/. Agents should load it before working on the application: it documents holm's routing, layout, page, action, and form conventions, and can answer questions about the library itself. See the holm skill command for managing it in other projects.

The application package

The composition root

app/main.py creates the FastAPI app, mounts static/ at /static, adds gzip compression, and hands everything to holm.App(). Custom FastAPI middleware, exception handlers, or additional mounts belong there.

Layout slots

Slots are configured in the same call. The root layout renders three named slots besides children: head (the <head> component from app/head.py), nav (the current-page-aware navigation bar from app/nav.py), and theme_switcher. Nested layouts can define further slots.

Conventions

Within app/, holm's conventions apply: page.py serves GET requests for its package's path, layout.py (or layout.jinja) wraps the pages of its package and everything below it, and actions.py defines endpoints that return HTML fragments, a natural fit for HTMX. error.py maps error codes to handlers; the 404 handler redirects to /not-found. app/layout.py is optional. holm renders layout.jinja on its own when no Python counterpart exists; the file exists to make the mechanism explicit and to serve as a starting point for programmatic layouts.

Settings and metadata

app/head.py renders the <head> and reads page metadata (such as this page's title) from the htmy context. app/settings.py holds runtime settings via pydantic-settings, reading .env when present, including which stylesheet and JS bundle to serve: the CSS_FILE and JS_FILE environment variables behind poe dev and poe preview.

Stylesheets and scripts

Python is not bundled; only the stylesheet and the JS bundle are built. Sources live in assets/ (app.css for your CSS, app.js for your scripts) and are compiled into static/ in two flavors:

SourceDev buildProduction build
assets/app.css static/app-dev.css: unminified, rebuilt on every save static/app.css: minified, in the repo
assets/app.js static/app-dev.js: unminified, rebuilt on every save static/app.js: minified, in the repo

The minified files belong in the repo so a fresh checkout can run and deploy without a production build. The dev files are build artifacts that can be deleted and regenerated at any time. Which pair is served is decided by CSS_FILE and JS_FILE from app/settings.py, rendered into <head> by app/head.py.

Build tasks

All build commands are poe tasks (build-dev-css, build-prod-css, build-dev-js, build-prod-js; poe build runs the production builds) delegating to the Tailwind CLI and the JS bundler. The package.json exists for these tools, and there is no application JavaScript beyond what assets/app.js imports.

The component catalog

The components/ directory is a copy of the htmui BasecoatUI catalog: pure Python htmy components in your project, not a locked dependency. Open a file and change it. Import them as from components.button import button.

Development workflow

uv run poe start          # http://localhost:5000

That runs three processes together via honcho, as defined in the Procfile:

The app is the first process in the Procfile, and honcho assigns ports to processes in that order, so the app gets port 5000. Edit assets/, app/, or components/, save, refresh.

Run the application standalone (for example uv run poe dev or fastapi dev) and it comes up on the FastAPI default port instead, which is 8000. poe dev serves the dev stylesheet and JS bundle; poe preview serves the minified files, which is what will be deployed, so use it to verify production assets.

Tooling and quality

Deployment

A holm application is a plain FastAPI application, so it deploys anywhere FastAPI is supported as a first-class citizen. Deployments serve the minified builds, so build them first:

uv run poe build

Providers that support the [tool.fastapi] configuration in pyproject.toml work out of the box with no extra configuration, for example: