Skip to content

For coding agents

This is the operating manual for generating QuadKit code. Humans can skip it and start at Your First App.

  1. /llms.txt — identity, install, rules, every docs URL.
  2. /agents.md — distilled framework AGENTS.md: hierarchy, Result vs exceptions, provider lifecycle, the never-list.
  3. /SKILL.md — drop-in skill for Claude Code, Cursor, OpenCode (same rules, fetchable without another clone).
  4. /llms-full.txt — the same catalog plus architecture notes. Per-package: /llms/index.txt (example /llms/quadkit-web.txt).
  5. This page — repo map, the one project tree, recipes. Then Common mistakes for fail vs fix.
  6. The generator output you just scaffolded with quadkit new project.
  7. The protocol in quadkit-contracts, not a concrete class in an extension.

Installable 10-skill pack: quadkit-skills (npx skills add dbtinoy-/quadkit-skills for Claude Code, Cursor, Codex, OpenCode). One-file fetch: /SKILL.md. Install commands & catalog: Agent skills. Do not re-derive the rules from blog posts. If a skill and this site ever disagree on layout, this site wins (domains/, app-root di/).

The repository’s AGENTS.md contains contributor guidance; use .importlinter and CI configuration to see the exact package-boundary checks.

contracts Shared protocols and types; no QuadKit implementation imports.
↑
core Low-level runtime; has declared third-party dependencies.
↑
extensions Regular peer boundaries are checked by import-linter.

When packages share a protocol, type, or exception, its intended home is quadkit-contracts rather than a peer implementation.

The published 0.0.50 line has eighteen packages: quadkit, quadkit-contracts, quadkit-web, quadkit-http, quadkit-sql, quadkit-cache, quadkit-storage, quadkit-events, quadkit-tasks, quadkit-monitor, quadkit-auth, quadkit-cli, quadkit-testing, quadkit-ai-llm, quadkit-ai-agents, quadkit-ai-mcp, quadkit-workflow, and quadkit-notification. All are installable at 0.0.50. Anything else you remember from an older revision of these docs is not installable — do not add it to pyproject.toml, and do not import it.

The root .importlinter configuration separates checks for contracts, core and foundational packages, regular extension peers, and AI/multimedia sub-packages. Composite packages—including admin, CLI, testing, and the AI facade—are intentionally treated differently, and specific tracked cross-package allowances are listed there. Run the configured checks with make lint-boundaries; this is not a blanket ban on every cross-package import.

There is one layout and currently one api project starter. Use quadkit add to add installed packages and quadkit gen to grow the application without changing its shape. There is no --structure flag and no models/ directory.

KindWhere it lives
Composition rootsrc/<app>/app.py — create_app(), ASGI target <app>.app:app
Unscoped HTTPsrc/<app>/controllers/
Unscoped domain typessrc/<app>/domains/
App providerssrc/<app>/di/*_provider.py
Module providersrc/<app>/modules/<slug>/provider.py
Cross-cuttingsrc/<app>/shared/ (errors, middleware, health, …)
Bounded contextsrc/<app>/modules/<slug>/ after quadkit new module
src/<app>/
├── app.py # create_app() — never list controllers by hand
├── controllers/
├── domains/
├── di/ # app-root providers
├── services/
├── infrastructure/
├── shared/
└── modules/
├── __init__.py # empty until quadkit new module
└── auth/
├── protocols.py # the only types other modules import
├── provider.py
├── domains/
└── controllers/

Controllers are discovered. Teach WebModule.configure(discover=["<app>.controllers", "<app>.modules"]) so quadkit gen controller does not require editing app.py. WebModule.configure() also accepts controllers=[…] when you want an explicit list. Adding a module lists it in create_app() next to WebModule.

Full generator map: Project Structure and CLI PROJECT_LAYOUT.

4. Repo map (dbtinoy-/quadkit, branch main)

Section titled “4. Repo map (dbtinoy-/quadkit, branch main)”
PathWhat it is
AGENTS.mdCanonical agent rules
core/quadkitApplication, container, providers, modules, Result, config, logging
core/quadkit-contractsProtocols, frozen DTOs, enums, exceptions — zero deps
packages/quadkit-web, packages/quadkit-httpASGI HTTP server, controllers, middleware, OpenAPI, and async HTTP client
packages/quadkit-ai-llm, packages/quadkit-ai-agents, packages/quadkit-ai-mcpLLM clients, tool-using agents & crews, and MCP server/client
packages/quadkit-sql, packages/quadkit-cache, packages/quadkit-storageAsync SQL repositories/UoW, multi-backend cache, and blob storage
packages/quadkit-events, packages/quadkit-notification, packages/quadkit-tasks, packages/quadkit-workflowCQRS/event sourcing, notifications/inbox, background workers, and workflows/sagas
packages/quadkit-auth, packages/quadkit-monitorJWT/OAuth2/RBAC auth and Prometheus/OpenTelemetry/health checks
packages/quadkit-cli, packages/quadkit-testingScaffolding, generators, dev runner, and test harnesses/fakes/compliance suites

Install and scaffold:

Terminal window
uv add "quadkit-cli>=0.0.50,<0.1"
quadkit new project my-app --template api
cd my-app
quadkit run

Add surface area with the CLI, then fill in behavior:

Terminal window
quadkit gen controller users # src/my_app/controllers/…
quadkit gen service greetings # src/my_app/services/…
quadkit gen error not_found # src/my_app/shared/errors/… (always shared)
quadkit new module auth # src/my_app/modules/auth/{protocols,provider,services}
quadkit gen controller users --module auth

A provider binds in register() and resolves in boot(). They take different protocols:

from quadkit.di.provider import Provider
from quadkit.contracts.core import ProviderPriority
from quadkit.contracts.core.di import (
ContainerRegistrarProtocol,
BootContainerProtocol,
)
class BillingProvider(Provider):
name = "billing"
priority = ProviderPriority.DOMAIN
async def register(self, container: ContainerRegistrarProtocol) -> None:
from my_app.services.billing_service import BillingService
container.singleton(BillingService, BillingService)
async def boot(self, container: BootContainerProtocol) -> None:
billing = await container.resolve(BillingService)
await billing.warmup()

App-root providers land in src/<app>/di/. Do not put business logic on the provider class.

Domain failures are values. Infrastructure failures raise:

from quadkit.result import Result, Ok, Err
async def find_user(self, user_id: str) -> Result[User, DomainError]:
user = await self.repo.get(user_id)
if not user:
return Err(UserNotFound(user_id))
return Ok(user)

Never unwrap() without is_ok(). Never return Result from __init__ or lifecycle hooks. Never wrap a dead database in Err.

quadkit new project boots the real Application — real DI, real providers. Read what it writes before writing anything else:

Terminal window
quadkit new project my-app --template api
find my-app/src -name '*.py' | sort
NeedStart from
A route with request validationquadkit gen controller <name>
A service behind a protocolquadkit gen service <name>
A typed domain errorquadkit gen error <name>
A bounded context with its own protocolsquadkit new module <slug>
A test that boots the appquadkit-testing (AppTestBed, WebTestBed, QuadkitContainerHarness) — see Testing

If the generated app does not look like what you were about to write — application.yaml, module.py / app.py as composition root, a provider that only registers, services that return Result[T, E] — it will not look like QuadKit.

  • Cite packages with the quadkit- prefix (quadkit-web, not “the web layer”).
  • Talk to the protocol in contracts, not the class in the extension.
  • Link the matching page on oridecon.dev.
  • FastAPI users are not starting over: keep Starlette routing and Pydantic; add a composition root. See Coming from FastAPI and the migration guide.
  • Pin >=0.0.50,<0.1 (documented release 0.0.50). Alpha means the surface can move; the architecture laws will not. Changelog: 0.0.50.
  • Never reference an unpublished package as if it were installable — that is the fastest way to produce code that cannot run.

The never-list lives in /agents.md: no service locator, no module-level singletons, no Any on injected constructors, no mocks in src/, no if/elif type dispatch.