Project anatomy

What's in your project, and where it goes

Everything fabbro create put on disk, and the one rule that governs each folder. Learn this map once and you'll always know where a change belongs.

The one rule: core/ is the framework — you import it and never edit it. Everything you build lives in modules/, db.py, app.py, and workers/, plugged in at fixed anchors. Settings and growing lists live in config/; secrets never touch the code.

Root files

The project's entry points and metadata, all at the top level.

app.py — the application factory and every route. Thin: it parses a request, calls a module, returns a page or JSON. New routes go at # <fabbro:routes>.

db.py — the project's schema, SQL, and data functions; the one place with SQL. New tables go at -- <fabbro:schema>, new functions at # <fabbro:data_functions>.

forjedoctor.py — the pre-flight checker that fabbro doctor runs. Its filename is a sentinel the CLI looks for to locate the project root — don't rename it.

agent.md — the guide you load into an AI agent: the non-negotiable rules and where things live.

requirements.txt · requirements-dev.txt · Dockerfile · docker-compose.yml · .gitignore — dependencies, the production image, and the local dev stack (web + workers + volume).

core/ — the framework

Import it, never edit it. This is Fabbro itself: reusable, domain-agnostic infrastructure your project stands on. Editing it means you've forked the framework.

modules/ — your business logic

One file per module (auth.py ships as the reference). A module holds only logic — rules, orchestration. It calls db.py for data and core/ for infrastructure; it never writes raw SQL or its own security. Each module defines a typed exception and raises it with an actionable message.

workers/ — async work

Background jobs, each a separate process, talking to the app only through the database queue. default.py and mailer.py ship as examples; a worker registers tasks with @worker.task(...) and ends with worker.run(queue=...). Web and workers must point at the same local database file.

config/ — settings, no hardcoding

One YAML per core module. This is where configurable values (Argon2 cost, timeouts) and growing lists (the CSP, allowlists) live — edit YAML, not code. Security contracts that must never be switched off (WAL, HttpOnly cookies) stay fixed in the code on purpose. A missing key fails loudly rather than booting on a silent default.

security.yaml · database.yaml · auth.yaml · worker.yaml · mailer.yaml · registry_api.yaml

Secrets never live here — a YAML may name a secret, never hold its value. Secrets go in .env or the Fernet vault, resolved at runtime.

templates/ & static/ — the UI

templates/ holds the Jinja pages and the three base layouts (layout_auth, layout_public, layout_app) that own the shell and the security contract (CSRF field, CSP nonce, no-flash theme). static/ is the design system — one CSS file, one JS file, one icon sprite, self-hosted fonts. To rebrand, override the tokens in static/css/fabbro.css; it propagates everywhere. See the Frontend Reference for the full vocabulary.

Data & secrets on disk

Created as you run, and kept out of git by .gitignore:

.env — your secrets (FERNET_KEY, SECRET_KEY). Never committed.

app.db — the SQLite database, created on first boot.

_dev/ & docs/ — for you, not production

_dev/ holds these playbooks, the playbook server (serve.py), and skills/ + prompts/ to load into an AI agent. It's excluded from the production build — it never ships. docs/ holds the Backend and Frontend references.

The mental model: a request enters at app.py, which calls a modules/ function, which reads or writes through db.py and leans on core/ for security, config, the queue, and external APIs. Slow work is handed to a worker. You add code at the anchors; the framework stays untouched.
← Back to playbooks