The AI-friendly
Python framework
Typed contracts and protocols for coding agents, with web, configuration, and testing foundations wired through one composition root.
uv add "quadkit-cli>=0.0.50,<0.1" Two doors into QuadKit
Two ways in: start from the terminal with your coding agent, or boost the starter in QuadKit Builder first — both end in a real, standalone QuadKit project.
FROM THE TERMINAL
01 Build with your coding agent
A Python backend an LLM can actually operate: typed protocols, a documented registry of 440 error codes, an explicit boot graph, and docs that ship in the format agents already fetch. Not autocomplete — build.
FROM THE CANVAS
02 Shape the starter, then build with your agent
QuadKit Builder lets you shape the graph visually, then exports a complete project — domains, providers, modules, and controllers wired. Your coding agent starts from that structure and spends its time on your features, not scaffolding.
Drag domains, providers & modules on canvas
I'm building a Python backend with the QuadKit framework. The starter is already generated. First read https://oridecon.dev/agents.md and follow it as your operating manual, then extend the starter to build my feature.
QuadKit never duplicates its rules. One sentence, one URL: the operating manual is oridecon.dev/agents.md.
# composition root — quadkit run boots this
from quadkit import Application, QuadKitConfig
from quadkit.web import WebModule
from my_app.di.orders_provider import OrdersProvider
def create_app(
config: QuadKitConfig | None = None
) -> Application:
app = Application(name="my-app", config=config)
app.add_modules([
WebModule.configure(
discover=["my_app.controllers"],
),
])
app.add_providers([OrdersProvider()])
return app
app = create_app() Application boot
Example boot · 4 providers
One command. This example uses the starter defaults; the container wires typed contracts and providers without extra application setup.
How it feels
An orders API, in four files
A controller, a service that returns Result, a provider that binds a protocol, then create_app().
Swap the implementation in the container — the controller never notices.
# @get marks the route; returning a Result renders as JSON or a problem response
from quadkit.web import Controller, get
from my_app.services.orders import OrderService
class OrdersController(Controller):
prefix = "/orders"
def __init__(self, orders: OrderService) -> None:
self._orders = orders
@get("/{order_id}")
async def show(self, order_id: str):
return await self._orders.find(order_id) # Expected failures are values; infrastructure failures still raise
from quadkit.result import Result, Ok, Err
class OrderService:
def __init__(self, repo: OrderRepository) -> None:
self._repo = repo
async def find(self, order_id: str) -> Result[Order, OrderNotFound]:
order = await self._repo.get(order_id)
if order is None:
return Err(OrderNotFound(order_id))
return Ok(order) # register() binds, boot() resolves — the two never share a method
from quadkit.di.provider import Provider
from quadkit.contracts.core import ProviderPriority
from quadkit.contracts.core.di import (
ContainerRegistrarProtocol,
BootContainerProtocol,
)
class OrdersProvider(Provider):
name = "orders"
priority = ProviderPriority.DOMAIN
async def register(self, container: ContainerRegistrarProtocol) -> None:
container.singleton(OrderRepository, InMemoryOrderRepository)
async def boot(self, container: BootContainerProtocol) -> None:
self._repo = await container.resolve(OrderRepository) # composition root — list the modules this app actually uses
from quadkit import Application, QuadKitConfig
from quadkit.web import WebModule
from my_app.di.orders_provider import OrdersProvider
def create_app(config: QuadKitConfig | None = None) -> Application:
app = Application(name="my-app", config=config)
app.add_modules([
WebModule.configure(discover=["my_app.controllers", "my_app.modules"]),
])
app.add_providers([OrdersProvider()])
return app The stack
Built so agents don't guess
The same seams that keep a human codebase composable are the ones a coding agent can inspect, type-check, and test against.
Contracts, not coupling
Every package talks through protocols in quadkit-contracts. Agents code to interfaces, not implementations. Swap a backend behind your own protocol — same contract, one config line.
Providers with a boot order
Lifecycle and wiring live in one place. Register, boot, shut down. Priority is explicit — infrastructure providers boot before the web layer mounts.
Result, not exceptions
Domain failures are Ok / Err. Agents can follow the error path without guessing which exception flies out of a handler.
Import linter
Architectural boundaries are enforced. Cross-package leaks fail CI before they ship — agents can verify their own imports.
Web & OpenAPI
ASGI controllers, routing, middleware, CORS, rate limiting. Docs generate themselves at /docs.
Typed end to end
Python 3.11+, async-first APIs, and full type hints. Your IDE and your agent see the same signatures.
Ecosystem
Install only what you use
Shared interfaces live in quadkit-contracts; configured package-boundary rules keep the ecosystem composable, with explicit allowances for composite packages and tracked edges.
The catalog has 18 packages, all published in 0.0.50.
The shape of an app
One composition root.
Three moves.
Every QuadKit app boots the same way. Deterministic startup means an agent can trace — and safely change — the whole system from a single file.
register() Modules bind protocols to implementations. register() never resolves anything — no I/O, no hidden work at import time.
boot() The container resolves the whole graph in dependency order and hands back an app that's actually ready.
shutdown() Connections, pools, and clients tear down in reverse order. Clean exits — in dev, tests, and production.
my_app/
├── domains/ # feature types — no models/
├── controllers/ # discovered by WebModule
├── di/ # app providers at the root
├── shared/ # cross-cutting pieces
├── modules/ # one provider.py each
└── app.py # the composition root
The api starter creates this tree; add installed packages without changing the layout. Even logging stays grep-able: one core config key, json_format.
ARCHITECTURE
Architecture decisions
Six design decisions that solve real problems. Each one prevents a bug category that convention-based frameworks can't catch.
The container has two faces — by design
Most DI containers give you the whole object. QuadKit splits it into `ContainerRegistrar` during setup and `ContainerResolver` during runtime — two separate interfaces on the same object. You literally cannot resolve a dependency during registration because the type system won't let you. The bug "tried to resolve a service that hasn't been registered yet" is caught at decoration time, not runtime.
# During register() — only registrar visible
class UserProvider(Provider):
async def register(self, c: ContainerRegistrarProtocol):
c.singleton(UserService, UserService(c.resolve(UserRepo)))
# ^^^
# Wait — c is a Registrar, resolve() doesn't exist yet.
# This is a compile-time error, not a runtime surprise.
# During boot() — resolver unlocked
class UserProvider(Provider):
async def boot(self, c: BootContainerProtocol):
service = c.resolve(UserService) # ✓ now it works Modules hide their internals from each other
The resolver uses a `ContextVar` to track which module is currently booting, then checks a compiled visibility graph on every resolution. Module A cannot accidentally depend on an internal service of Module B — the resolver rejects it. This is genuine encapsulation at the DI level, not a convention you hope everyone follows.
# Module A exports UserService, hides UserRepo
# Module B tries to reach into Module A's internals
class ModuleB(Module):
async def boot(self, c: BootContainerProtocol):
repo = c.resolve(UserRepo) # ✗ rejected at runtime
# The resolver checks: is UserRepo visible to ModuleB?
# The compiled graph says no. Hard boundary. A six-phase compiler for your dependency graph
The module compiler runs a pipeline: collect → cycle detection → validation → re-export expansion → visibility → provider ordering. Phase 4 transitively expands re-exports so Module B can see Module A's public types without re-declaring them. Phase 6 topologically sorts providers into parallel boot levels. It's a tiny static analysis tool for your dependency graph, and it's extensible.
# The compiler pipeline
Collect → CycleDetection → Validation → ReexportExpansion
→ Visibility → ProviderOrdering
# Phase 4: Module A exports UserService
# Module B imports Module A
# → Module B can see UserService transitively
# Phase 6: Providers sorted into boot levels
# Level 1: [UserRepo, CacheProvider]
# Level 2: [UserService] ← depends on Level 1
# Level 3: [UserController] ← depends on Level 2 Parallel boot with automatic rollback
Providers within the same topological level boot concurrently via `asyncio.gather`. If any provider fails, all previously booted providers shut down in reverse order — a full transactional rollback. Required providers trigger the rollback; optional ones fail gracefully with a warning.
# Boot levels execute in parallel
Level 1: [CacheProvider, UserRepo] ← async.gather
Level 2: [UserService] ← async.gather
Level 3: [UserController] ← async.gather
# If UserService fails in Level 2:
# → UserController never boots
# → UserRepo.shutdown() called
# → CacheProvider.shutdown() called
# → Clean state, no leaked resources Lock-free concurrent resolution
When multiple async tasks request the same scoped service, the first creates an `asyncio.Future` and stores it. Subsequent requests await that same future instead of racing to create their own instance. No locks, no event loop blocking — cooperative deduplication. The result is cached on success; the future is cancelled on failure.
# Two coroutines request the same scoped service
async def handler_a(scope):
user = await scope.resolve(UserService) # creates instance
async def handler_b(scope):
user = await scope.resolve(UserService) # joins in-flight
# handler_b doesn't create a second instance.
# It awaits handler_a's future. Lock-free. SSRF protection that defeats DNS rebinding
The SSRF guard doesn't just check if a URL looks safe. It resolves the hostname to IP addresses, validates every resolved address against private ranges, and returns the validated set. The caller pins its connection to exactly those addresses. An attacker who makes DNS resolve to a public IP at validation time and a private IP at connect time gets caught.
# collect_safe_addresses() in quadkit-contracts
# 1. Resolve hostname → [IP, IP, ...]
# 2. Check ALL resolved addresses against private ranges
# 3. Return validated address set
# 4. Caller pins HTTP connection to those exact IPs
# DNS rebinding attack: DNS → 8.8.8.8 at validation,
# DNS → 192.168.1.1 at connect. Caught.
addrs = collect_safe_addresses("https://example.com")
# addrs = [IPv4Address('93.184.216.34')] # pinned CONTRACTS
Build anything with composable contracts
QuadKit ships typed contracts for AI/LLM, web, auth, storage, tasks, resilience, and multimedia — ready to wire into your app. Each contract is a protocol with error codes, providers, and generators. Mix them to build backends that scale.
from quadkit import Application, Provider
from quadkit.contracts.infra.cache import CacheBackendProtocol
# 1. implement the contract
class RedisCache(CacheBackendProtocol):
async def get(self, key: str):
return await self.redis.get(key)
# 2. bind it in a provider
class CacheProvider(Provider):
async def register(self, container):
container.singleton(
CacheBackendProtocol, RedisCache
)
# 3. boot the app
async with Application.boot(
providers=[CacheProvider()]
) as app:
cache = await app.container.resolve(
CacheBackendProtocol
) Coming from FastAPI
Keep the HTTP instincts. Add a composition root.
Starlette routing, Pydantic request shapes, OpenAPI — you already know this layer. QuadKit wraps it in a container, providers, and a contract-first ecosystem so services, configuration, and tooling feel as designed as the routes. Feature types live in domains/, not a models/ directory.
FAQ
Answers, for humans and agents.
The honest version: what this is, what stage it's at, and where the real documentation lives.
Open the docsAn async-first, contract-driven Python application framework. The core gives you a DI container, providers, modules, YAML config, and the Result type. Published extensions cover web, data, events, tasks, security, tooling, AI/LLM, agents, MCP, workflow, and notifications — all 18 catalog packages publish at 0.0.50. Extensions talk through protocols — never through one another.
No. Starlette routing, Pydantic request shapes, and OpenAPI remain the HTTP layer; QuadKit adds a composition root around them — constructor injection instead of Depends(), providers instead of ad-hoc startup hooks, and contracts so a backend can be swapped in config. Adopt it one service at a time.
Coding agents need stable interfaces, typed errors, and docs they can load. QuadKit uses typed protocols, a registry of 440 documented error codes, /llms.txt, /agents.md, /SKILL.md, the quadkit-skills pack, and fail-vs-fix guidance. Import boundaries are linted so generated code cannot quietly couple packages.
Python 3.11 or newer. QuadKit is async-first. Install the current CLI with uv add "quadkit-cli>=0.0.50,<0.1" (or pip install "quadkit-cli>=0.0.50,<0.1"), then run quadkit new project my-app --template api.
QuadKit is alpha (0.0.x) — 0.0.50 today. Public APIs may change before 1.0 — pin versions in production and follow the changelog. Treat it as early on purpose: the architecture is stable; the surface is still moving.
No. Install the CLI, scaffold, then add only the packages your service uses (for example, quadkit-web, quadkit-sql, and quadkit-testing). The foundation is quadkit plus quadkit-contracts; the AI/LLM, agent, MCP, workflow, and notification packages publish with the rest of the catalog. Extensions never depend on other extensions.
Updates
What's new
The 0.0.x line, plus writing from the last few weeks. Not a git log.
QuadKit 0.0.50 — the eighteen-package line
The open-core expansion is in the published line — AI, workflow, and notification packages ship with the thirteen at one version, with the guides and agent surfaces to match.
Nov 9, 2026
BlogQK Gatewai — the Community Edition for AI developers
One OpenAI-compatible gateway over free provider tiers, plus a deterministic offline stub — run and test agents locally with zero credentials and no spend.
Nov 6, 2026
BlogFrom Playground to Builder — rebuilding the QuadKit Builder
The node-canvas workbench that proved one-way codegen is becoming the QuadKit Builder at builder.quadkit.dev — with a trust layer and agent authoring on the roadmap.
Sep 25, 2026
Start building
Ship a backend your agent can keep working on
Install the CLI, scaffold with quadkit new project, and add packages as you need them. The contracts stay put.
uv add "quadkit-cli>=0.0.50,<0.1"