<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Huy Nguyen</title>
    <description>The latest articles on DEV Community by Huy Nguyen (@huynguyengl99).</description>
    <link>https://dev.to/huynguyengl99</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1654565%2F9f1c9a82-ef1d-447c-9b5c-f8ebaf086c6e.jpeg</url>
      <title>DEV Community: Huy Nguyen</title>
      <link>https://dev.to/huynguyengl99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/huynguyengl99"/>
    <language>en</language>
    <item>
      <title>Benchmarking Python API Frameworks with Real Workloads: FastAPI vs Litestar vs DRF vs Django Ninja vs Django Bolt</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:42:01 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/benchmarking-python-api-frameworks-with-real-workloads-fastapi-vs-litestar-vs-drf-vs-django-ninja-43g</link>
      <guid>https://dev.to/huynguyengl99/benchmarking-python-api-frameworks-with-real-workloads-fastapi-vs-litestar-vs-drf-vs-django-ninja-43g</guid>
      <description>&lt;p&gt;Most Python framework benchmarks measure "hello world" JSON. Real APIs do authentication,&lt;br&gt;
database access and complex queries - so that's what I benchmarked, across FastAPI,&lt;br&gt;
Litestar, Django REST Framework, Django Ninja and Django Bolt, each with multiple&lt;br&gt;
production servers, under strict Docker resource limits.&lt;/p&gt;

&lt;p&gt;Everything is reproducible: &lt;a href="https://github.com/huynguyengl99/python-api-frameworks-benchmark" rel="noopener noreferrer"&gt;https://github.com/huynguyengl99/python-api-frameworks-benchmark&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The core design decisions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Latest versions of everything&lt;/strong&gt; - Django Bolt 0.10, Django 6.0, FastAPI 0.141,
Litestar 2.24, DRF 3.18, Django Ninja 1.6.2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each framework uses its own native serializer.&lt;/strong&gt; Feeding Litestar Pydantic models
makes it convert twice (Pydantic then msgspec), so you'd be benchmarking Pydantic
instead of Litestar - a point the Litestar author raised on an earlier version of this
benchmark. Litestar and Bolt use msgspec structs, FastAPI and Ninja use Pydantic, DRF
uses its serializers. The response models are defined twice, field for field, and the
output is byte-identical: 4,796 bytes for &lt;code&gt;/articles/1&lt;/code&gt; from both FastAPI and Litestar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT in an httpOnly cookie for the auth endpoints&lt;/strong&gt; - the setup I'd actually use in
production. Each framework uses its own ecosystem's library instead of something
hand-rolled: drf-auth-kit (DRF), django-ninja-jwt (Ninja), AuthX (FastAPI), and the
built-in cookie JWT support in Litestar and Bolt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Median across container restarts, not repeated runs.&lt;/strong&gt; Some servers (notably Bolt's
Rust worker pool under a 1-CPU cgroup) pick a throughput regime at container startup
and hold it, so best-of-N inside one container flatters the lucky starts. Details in
the repo's Methodology section.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MacBook M2 Pro, 32GB RAM; PostgreSQL 16; 500 articles, 2000 comments, 100 tags, 50 authors&lt;/li&gt;
&lt;li&gt;Each framework alone in a Docker container: &lt;code&gt;--cpus=1&lt;/code&gt;, &lt;code&gt;--memory=750m&lt;/code&gt;, one at a time&lt;/li&gt;
&lt;li&gt;bombardier, 100 connections, 10s per measurement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 independent container starts per framework&lt;/strong&gt;, median reported with min-max spread&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Endpoints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/json-1k&lt;/code&gt;, &lt;code&gt;/json-10k&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pure serialization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 simple rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/articles?page=1&amp;amp;page_size=20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pagination + nested author/tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/articles/1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nested author + tags + comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/auth/me&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cookie JWT verify + load user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/auth/articles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cookie JWT + the paginated query&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One fairness detail that matters: &lt;strong&gt;all five frameworks load the user row from the DB on&lt;br&gt;
both auth endpoints.&lt;/strong&gt; Litestar, DRF and Ninja do it as part of authenticating; AuthX and&lt;br&gt;
Bolt's guards only verify the signature, so I made FastAPI and Bolt load the user&lt;br&gt;
explicitly. Otherwise they'd do strictly less work and the comparison would be junk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;RPS, median over 5 container starts, zero errors across all 70 measurements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;json-1k&lt;/th&gt;
&lt;th&gt;json-10k&lt;/th&gt;
&lt;th&gt;/db&lt;/th&gt;
&lt;th&gt;/articles&lt;/th&gt;
&lt;th&gt;/articles/1&lt;/th&gt;
&lt;th&gt;/auth/me&lt;/th&gt;
&lt;th&gt;/auth/articles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;bolt&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38,576&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19,089&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,986&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;208&lt;/td&gt;
&lt;td&gt;432&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,024&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;litestar-uvicorn&lt;/td&gt;
&lt;td&gt;31,284&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;24,547&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,039&lt;/td&gt;
&lt;td&gt;246&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;976&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;litestar-granian&lt;/td&gt;
&lt;td&gt;19,006&lt;/td&gt;
&lt;td&gt;15,166&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,180&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;250&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;488&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,104&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;210&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastapi-uvicorn&lt;/td&gt;
&lt;td&gt;13,845&lt;/td&gt;
&lt;td&gt;2,641&lt;/td&gt;
&lt;td&gt;984&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;td&gt;428&lt;/td&gt;
&lt;td&gt;820&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastapi-granian&lt;/td&gt;
&lt;td&gt;8,484&lt;/td&gt;
&lt;td&gt;2,280&lt;/td&gt;
&lt;td&gt;952&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;410&lt;/td&gt;
&lt;td&gt;749&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drf-gunicorn&lt;/td&gt;
&lt;td&gt;3,925&lt;/td&gt;
&lt;td&gt;3,132&lt;/td&gt;
&lt;td&gt;282&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;td&gt;261&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drf-granian&lt;/td&gt;
&lt;td&gt;2,703&lt;/td&gt;
&lt;td&gt;2,200&lt;/td&gt;
&lt;td&gt;830&lt;/td&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;321&lt;/td&gt;
&lt;td&gt;726&lt;/td&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ninja-granian&lt;/td&gt;
&lt;td&gt;1,566&lt;/td&gt;
&lt;td&gt;1,422&lt;/td&gt;
&lt;td&gt;680&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;295&lt;/td&gt;
&lt;td&gt;610&lt;/td&gt;
&lt;td&gt;117&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ninja-uvicorn&lt;/td&gt;
&lt;td&gt;1,533&lt;/td&gt;
&lt;td&gt;1,424&lt;/td&gt;
&lt;td&gt;699&lt;/td&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;td&gt;236&lt;/td&gt;
&lt;td&gt;584&lt;/td&gt;
&lt;td&gt;114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drf-uvicorn&lt;/td&gt;
&lt;td&gt;1,035&lt;/td&gt;
&lt;td&gt;973&lt;/td&gt;
&lt;td&gt;495&lt;/td&gt;
&lt;td&gt;153&lt;/td&gt;
&lt;td&gt;234&lt;/td&gt;
&lt;td&gt;447&lt;/td&gt;
&lt;td&gt;137&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The gap collapses once you hit the database
&lt;/h3&gt;

&lt;p&gt;37x between fastest and slowest on &lt;code&gt;/json-1k&lt;/code&gt;. &lt;strong&gt;1.9x&lt;/strong&gt; on &lt;code&gt;/articles/1&lt;/code&gt;. If your endpoint&lt;br&gt;
touches PostgreSQL, your framework is not the bottleneck - your queries are.&lt;/p&gt;

&lt;h3&gt;
  
  
  What authentication actually costs
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fou34kmqghe9l923tmt32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fou34kmqghe9l923tmt32.png" alt="RPS on /auth/me: Django Bolt 3,008, 2.7x ahead of the rest" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comparing &lt;code&gt;/auth/articles&lt;/code&gt; against the identical unauthenticated &lt;code&gt;/articles&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;public&lt;/th&gt;
&lt;th&gt;authed&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fastapi-granian&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;td&gt;-1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drf-gunicorn&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;-5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bolt&lt;/td&gt;
&lt;td&gt;208&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;td&gt;-6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;drf-granian&lt;/td&gt;
&lt;td&gt;198&lt;/td&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;td&gt;-10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ninja-uvicorn&lt;/td&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;td&gt;114&lt;/td&gt;
&lt;td&gt;-10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastapi-uvicorn&lt;/td&gt;
&lt;td&gt;224&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;td&gt;-14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;litestar-granian&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;210&lt;/td&gt;
&lt;td&gt;-16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;litestar-uvicorn&lt;/td&gt;
&lt;td&gt;246&lt;/td&gt;
&lt;td&gt;193&lt;/td&gt;
&lt;td&gt;-22%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly 5-20%, cheaper than I expected for "verify a token and load a user". Two things&lt;br&gt;
stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bolt's &lt;code&gt;/auth/me&lt;/code&gt; is 2.7x ahead of everything&lt;/strong&gt; (3,024 vs 1,104) because it validates
the JWT in Rust before Python runs at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Litestar pays the most&lt;/strong&gt; for a concrete reason: its &lt;code&gt;JWTCookieAuth&lt;/code&gt; runs in middleware,
before dependency injection, so &lt;code&gt;retrieve_user_handler&lt;/code&gt; opens its own DB session - that
request pays for two connection acquisitions instead of one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Memory and CPU
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgs1l5oydua2uguqk8ggj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgs1l5oydua2uguqk8ggj.png" alt="Memory and CPU per framework: most 195-260MB, drf-granian 456MB; Bolt lowest CPU at 67%" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most configs peak at 195-260MB RAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;drf-granian is the outlier at 456MB&lt;/strong&gt; - untuned &lt;code&gt;--blocking-threads&lt;/code&gt;, per the Granian
maintainer (see "unaddressed feedback" below)&lt;/li&gt;
&lt;li&gt;Nearly everything saturates ~85% of the 1-CPU budget under load - &lt;strong&gt;except Bolt at 67%&lt;/strong&gt;,
while leading most endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A word on Django Bolt
&lt;/h3&gt;

&lt;p&gt;If you're open to a young framework, this is the one I'd watch. It won or tied the top&lt;br&gt;
spot on 4 of 7 endpoints while leaving ~18% more CPU headroom than every other config -&lt;br&gt;
room to grow under load, not just a good number on a chart. And unlike moving to Litestar&lt;br&gt;
or FastAPI, you keep the Django ORM, admin and package ecosystem.&lt;/p&gt;

&lt;p&gt;Honest trade-offs: it's young and moving fast, the Rust internals (Actix Web + PyO3 +&lt;br&gt;
msgspec) mean you can't monkey-patch your way out of a corner, and under a hard 1-CPU cap&lt;br&gt;
its throughput varies between container starts under a hard 1-CPU cap. For a side project or a new&lt;br&gt;
internal service, I'd have no problem reaching for it today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback I have NOT addressed yet
&lt;/h2&gt;

&lt;p&gt;Being upfront, because it affects one result. The Granian maintainer (u/gi0baro) explained&lt;br&gt;
that drf-granian's memory number comes from me not setting &lt;code&gt;--blocking-threads&lt;/code&gt; or&lt;br&gt;
backpressure, so it spawns a lot of threads and burns time on GIL contention - drf-granian's&lt;br&gt;
456MB is that same unfixed issue. He also noted that &lt;code&gt;--cpus=1&lt;/code&gt; in Docker is a time-slice&lt;br&gt;
scheduler limit rather than a real core pin, and that it penalizes Granian more than other&lt;br&gt;
servers since Granian runs I/O in a separate runtime with extra threads.&lt;/p&gt;

&lt;p&gt;I'm keeping the CPU cap because it makes runs reproducible and comparable, but he's right&lt;br&gt;
on both counts. Tuning &lt;code&gt;--blocking-threads&lt;/code&gt; is top of my list for the next iteration, along with cold&lt;br&gt;
start time and image size (both suggested by the Litestar author).&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Framework choice barely matters once you touch I/O.&lt;/strong&gt; Optimize your queries - the
N+1 you didn't notice costs more than any framework migration will ever save.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For raw serialization, Bolt and Litestar lead&lt;/strong&gt;, then FastAPI, then a big gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie JWT auth costs 5-20%&lt;/strong&gt; on a DB-heavy endpoint; where the token is validated
(Rust vs Python, middleware vs dependency) decides where in that range you land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;uvicorn vs granian is workload-dependent&lt;/strong&gt;: uvicorn wins CPU-bound JSON on ASGI
frameworks, granian wins DB-bound endpoints, and granian clearly beats uvicorn for
WSGI DRF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure across container restarts and publish the spread&lt;/strong&gt; - it changed these
results more than any framework upgrade did.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything - code, Docker setup, runner, raw per-framework JSON results, and dated&lt;br&gt;
snapshots - is in the repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/huynguyengl99/python-api-frameworks-benchmark" rel="noopener noreferrer"&gt;https://github.com/huynguyengl99/python-api-frameworks-benchmark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Issues and PRs are very welcome, especially if you know these servers better than I do.&lt;br&gt;
And if you find it useful, a star would be appreciated 😄&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>fastapi</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>Streaming Agents with Pydantic AI over Typed WebSockets</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Sat, 08 Aug 2026 14:34:43 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/streaming-agents-with-pydantic-ai-over-typed-websockets-215i</link>
      <guid>https://dev.to/huynguyengl99/streaming-agents-with-pydantic-ai-over-typed-websockets-215i</guid>
      <description>&lt;p&gt;I've been building realtime products for years: group chat, voice pipelines, streaming AI assistants. Two kinds of pain kept following me around.&lt;/p&gt;

&lt;p&gt;The first was WebSockets. Every project grew the same untyped &lt;code&gt;receive_json&lt;/code&gt; handler with an if/else chain, hand-written validation, and no contract the frontend could rely on. Messages were stringly-typed, bugs surfaced at runtime, and onboarding a teammate meant "read the consumer, end to end". That pain is why I built &lt;a href="https://github.com/huynguyengl99/chanx" rel="noopener noreferrer"&gt;chanx&lt;/a&gt;: typed messages, automatic routing, AsyncAPI docs (the full story is in &lt;a href="https://huynguyengl99.github.io/posts/chanx-structured-websockets-django-fastapi/" rel="noopener noreferrer"&gt;my intro post&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The second was agent frameworks. I shipped my first LLM features on LangChain and spent more time debugging its abstractions than my own code. &lt;a href="https://pydantic.dev/docs/ai/" rel="noopener noreferrer"&gt;Pydantic AI&lt;/a&gt; won me over the first time I tried it: typed tools, typed outputs, typed dependency injection. It applies to agents the same philosophy chanx applies to WebSockets, and since both are built on Pydantic they snap together with almost no glue.&lt;/p&gt;

&lt;p&gt;There's just one gap. At the UI layer, pydantic-ai's official story is SSE-shaped: adapters for &lt;a href="https://pydantic.dev/docs/ai/integrations/ui/overview/" rel="noopener noreferrer"&gt;AG-UI and the Vercel AI protocol&lt;/a&gt;, and a built-in &lt;a href="https://pydantic.dev/docs/ai/guides/web/" rel="noopener noreferrer"&gt;&lt;code&gt;Agent.to_web()&lt;/code&gt;&lt;/a&gt; chat page that the docs explicitly label as a dev tool. Real agent products want more than a one-way stream. A conversation is &lt;em&gt;bidirectional&lt;/em&gt;: the user sends prompts, the agent streams back text and tool activity, and (the interesting part) the agent sometimes has to &lt;strong&gt;stop and ask permission&lt;/strong&gt; before running a destructive tool. That round trip (pause, ask, resume) fits a WebSocket perfectly, and there is currently no WebSocket example anywhere in the pydantic-ai docs.&lt;/p&gt;

&lt;p&gt;So I built one, the same way I build REST APIs: &lt;strong&gt;contract-first&lt;/strong&gt;. The server defines every message as a Pydantic model, chanx turns those models into routed handlers plus an AsyncAPI 3.0 schema, and the React client generates its TypeScript types &lt;em&gt;from&lt;/em&gt; that schema. Backend and frontend can't drift, teammates integrate from the contract instead of reading your Python, and the whole thing (streaming, tool calls, approvals) is testable without ever calling an LLM.&lt;/p&gt;

&lt;p&gt;Companion repo with everything runnable: &lt;a href="https://github.com/huynguyengl99/pydantic-ai-ws-agent" rel="noopener noreferrer"&gt;huynguyengl99/pydantic-ai-ws-agent&lt;/a&gt;. Snippets below are trimmed for reading and current as of pydantic-ai 2.9 and chanx 2.8; the repo is the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big picture
&lt;/h2&gt;

&lt;p&gt;The demo agent ("Tasklet") manages a task list in SQLite. Safe tools run freely; destructive ones pause for approval.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7bkl8ez8t4ahg48bk20.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7bkl8ez8t4ahg48bk20.png" alt="Tasklet: live tool cards, a destructive tool paused for approval, and the per-conversation task list" width="800" height="566"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pydantic-ai Agent ──run_stream events──▶ AgentHarness (background task)
      │                                        │ broadcast_event
  typed tools                                  ▼
  requires_approval=True          group: conversation.{id}   ◀── HTTP POST /notify
                                               │ channel layer (in-memory / Redis)
                              ┌────────────────┼────────────────┐
                              ▼                ▼                ▼
                      chanx consumer    chanx consumer   chanx consumer      AsyncAPI 3.0 schema
                        (tab 1)           (tab 2)        (other device)  ──▶ generated TS types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four pieces: a plain pydantic-ai &lt;strong&gt;agent&lt;/strong&gt; (typed tools, typed deps, &lt;code&gt;requires_approval&lt;/code&gt; on the dangerous ones, structured output); a &lt;strong&gt;harness&lt;/strong&gt; that runs it and translates each event into a typed message; a &lt;strong&gt;channel layer&lt;/strong&gt; that carries those events to a per-conversation group rather than a single socket; and a chanx &lt;strong&gt;consumer&lt;/strong&gt; that validates, routes, and documents the protocol the React client generates its types from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent: tools that know they're dangerous
&lt;/h2&gt;

&lt;p&gt;Nothing here is WebSocket-aware. It's a standard pydantic-ai agent with two deliberate choices: a structured output type, and typed deps carrying the conversation id so every tool operates on that conversation's tasks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TextAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Final answer to the user, with suggested follow-up prompts.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;follow_ups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="c1"&gt;# Every run ends as either a final answer or a request for tool approval.
&lt;/span&gt;&lt;span class="n"&gt;AgentOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TextAnswer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DeferredToolRequests&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AgentOutput&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AgentOutput&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolved_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;deps_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TextAnswer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeferredToolRequests&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;INSTRUCTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@agent.tool&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TaskItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Add a new task with the given title.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@agent.tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requires_approval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Permanently delete the task with the given id. Requires user approval.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; deleted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The repo has six tools; &lt;code&gt;clear_all_tasks&lt;/code&gt; is the other one behind approval.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;requires_approval=True&lt;/code&gt; is pydantic-ai's &lt;a href="https://pydantic.dev/docs/ai/tools-toolsets/deferred-tools/" rel="noopener noreferrer"&gt;deferred tools&lt;/a&gt; feature. When the model calls &lt;code&gt;delete_task&lt;/code&gt;, the tool does &lt;strong&gt;not&lt;/strong&gt; execute. Instead the run ends early and &lt;code&gt;result.output&lt;/code&gt; is a &lt;code&gt;DeferredToolRequests&lt;/code&gt; carrying the pending calls, which is exactly why &lt;code&gt;output_type&lt;/code&gt; is a union: every run ends as either a final answer or a request for permission, and the type system makes you handle both.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TextAnswer&lt;/code&gt; instead of plain &lt;code&gt;str&lt;/code&gt; is the structured-output half of the story. Alongside the answer text, the model fills &lt;code&gt;follow_ups&lt;/code&gt; with three suggested next prompts that the UI renders as clickable chips. The final answer isn't prose with conventions bolted on; it's a validated object, and the chips ride along in the same schema.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;build_agent()&lt;/code&gt; being a factory instead of a module-level singleton matters later: tests build the same agent with a fake model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The message contract
&lt;/h2&gt;

&lt;p&gt;Every WebSocket message is a Pydantic model with a &lt;code&gt;Literal&lt;/code&gt; action field. chanx uses that field as a discriminator for validation, routing, and docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;User sends a prompt to the agent.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatPayload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full protocol is small. Client → server: &lt;code&gt;chat&lt;/code&gt;, &lt;code&gt;tool_decision&lt;/code&gt;. Server → client: &lt;code&gt;user_message&lt;/code&gt;, &lt;code&gt;stream_start&lt;/code&gt;, &lt;code&gt;text_delta&lt;/code&gt;, &lt;code&gt;tool_call&lt;/code&gt;, &lt;code&gt;tool_result&lt;/code&gt;, &lt;code&gt;approval_request&lt;/code&gt;, &lt;code&gt;stream_end&lt;/code&gt;, &lt;code&gt;suggestions&lt;/code&gt;, &lt;code&gt;history&lt;/code&gt;, &lt;code&gt;tasks_updated&lt;/code&gt;, &lt;code&gt;notification&lt;/code&gt;, &lt;code&gt;agent_error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The consumer binds it together: handlers declare what they accept and what they may reply with, and that declaration &lt;em&gt;is&lt;/em&gt; the documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pydantic AI task assistant over a typed WebSocket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseConsumer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;passthrough_events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ClassVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;]]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BROADCAST_MESSAGES&lt;/span&gt;

    &lt;span class="nd"&gt;@ws_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Send a prompt to the agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AgentServerMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# the union of everything we may send back
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_spawn_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="nd"&gt;@ws_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Approve or deny pending tool calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AgentServerMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_tool_decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolDecisionMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_spawn_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No if/else routing, no manual validation, and &lt;code&gt;/asyncapi.json&lt;/code&gt; now serves an AsyncAPI 3.0 spec describing every message above: the WebSocket equivalent of what drf-spectacular gives you for REST (I wrote about that workflow in &lt;a href="https://huynguyengl99.github.io/posts/schema-first-api-development-drf-react/" rel="noopener noreferrer"&gt;the schema-first post&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming structured output: snapshots into deltas
&lt;/h2&gt;

&lt;p&gt;Streaming plain text is easy. Streaming a &lt;em&gt;structured&lt;/em&gt; answer is the interesting part: the final output is a &lt;code&gt;TextAnswer&lt;/code&gt; object, so there is no raw text stream to forward. pydantic-ai's answer is &lt;code&gt;run_stream()&lt;/code&gt; plus two hooks.&lt;/p&gt;

&lt;p&gt;Tool events arrive through an &lt;code&gt;event_stream_handler&lt;/code&gt;, which the harness maps straight onto the message contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward_tool_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncIterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AgentStreamEvent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;FunctionToolCallEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCallMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ToolCallPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;args_as_dict&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;FunctionToolResultEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolResultMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ToolResultPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;to_jsonable_python&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The answer itself streams as &lt;em&gt;partially validated snapshots&lt;/em&gt;: &lt;code&gt;stream_output()&lt;/code&gt; yields a growing &lt;code&gt;TextAnswer&lt;/code&gt; each time more of the model's JSON arrives. Diffing successive &lt;code&gt;content&lt;/code&gt; values recovers the text deltas to send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sent_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message_history&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deferred_tool_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deferred_tool_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AgentDeps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;event_stream_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;forward_tool_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debounce_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;TextAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;sent_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sent_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sent_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TextDeltaMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;TextDeltaPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="n"&gt;sent_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all_messages_json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client sees &lt;code&gt;text_delta&lt;/code&gt; for prose and &lt;code&gt;tool_call&lt;/code&gt;/&lt;code&gt;tool_result&lt;/code&gt; pairs for tool activity (rendered as live cards in the UI), and the loop stays completely ignorant of &lt;em&gt;which&lt;/em&gt; tools exist.&lt;/p&gt;

&lt;p&gt;After the loop, the union output type earns its keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;DeferredToolRequests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# run_stream stops forwarding events once the final result (the paused
&lt;/span&gt;        &lt;span class="c1"&gt;# calls) is found, so announce them here: same ids, same wire order.
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approvals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCallMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ToolCallPayload&lt;/span&gt;&lt;span class="p"&gt;(...)))&lt;/span&gt;
        &lt;span class="n"&gt;PENDING_APPROVALS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;approval_request_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;TextAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;follow_ups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;follow_ups&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StreamEndMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;StreamEndPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;...)))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;follow_ups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_suggestions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;follow_ups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SuggestionsMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;SuggestionsPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;follow_ups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;follow_ups&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtlety worth calling out: a deferred tool call never reaches the &lt;code&gt;event_stream_handler&lt;/code&gt; (deferred means "not executed", and only executing tools emit events), so the harness announces the paused calls itself before the &lt;code&gt;approval_request&lt;/code&gt;. The client renders the same tool cards either way; it can't tell which path produced them. And on the happy path, the suggestions arrive as their own message &lt;em&gt;after&lt;/em&gt; &lt;code&gt;stream_end&lt;/code&gt;, so the chips pop in once the answer settles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broadcast, don't send: background runs and groups
&lt;/h2&gt;

&lt;p&gt;My first version awaited the agent run &lt;em&gt;inside&lt;/em&gt; the chat handler and wrote messages straight to the socket. It worked, but with three quiet problems: the handler blocks for the whole run, a page refresh mid-run kills the stream, and a second tab sees nothing.&lt;/p&gt;

&lt;p&gt;The fix is the pattern chanx (and Django Channels before it) is built around: &lt;strong&gt;broadcast to a group, not to a connection&lt;/strong&gt;. Every consumer joins a per-conversation group on connect, and the run executes as a background task that broadcasts events to that group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_authentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;harness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentHarness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_broadcast&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_spawn_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# The task outlives this connection: a client that refreshes mid-run
&lt;/span&gt;    &lt;span class="c1"&gt;# keeps receiving events through the conversation group.
&lt;/span&gt;    &lt;span class="n"&gt;RUNNING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;guarded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the receiving side there is nothing to write. chanx's &lt;code&gt;passthrough_events&lt;/code&gt; generates the event handlers: any message type in the list that arrives via the group is forwarded to the WebSocket client verbatim, and documented in AsyncAPI like everything else.&lt;/p&gt;

&lt;p&gt;What this buys, concretely (all covered by tests):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The chat handler returns immediately&lt;/strong&gt;; a per-conversation &lt;code&gt;RUNNING&lt;/code&gt; dict rejects concurrent runs with a typed &lt;code&gt;agent_error&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh-proof streams&lt;/strong&gt;: the run keeps going without any socket attached; reconnect replays history and picks up live events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tab for free&lt;/strong&gt;: every tab renders the same deltas, tool cards, and approval requests. Even the user's own prompt travels this way, echoed as a &lt;code&gt;user_message&lt;/code&gt; before &lt;code&gt;stream_start&lt;/code&gt;, so the sending tab renders the bubble from the same broadcast as everyone else instead of appending locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve from anywhere&lt;/strong&gt;: pending approvals are keyed by conversation, not connection; request approval on one device, approve on another&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Beyond the socket: HTTP, workers, other frameworks
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;broadcast_event&lt;/code&gt; is a classmethod, so &lt;em&gt;anything&lt;/em&gt; can push typed messages into a conversation. The demo exposes &lt;code&gt;POST /conversations/{id}/notify&lt;/code&gt;, and a &lt;code&gt;curl&lt;/code&gt; fires a toast on every connected tab. The agent uses the same door from the inside: a &lt;code&gt;schedule_reminder&lt;/code&gt; tool reads the conversation id from &lt;code&gt;ctx.deps&lt;/code&gt;, schedules a job, and returns immediately; when the job fires long after &lt;code&gt;stream_end&lt;/code&gt;, it broadcasts a &lt;code&gt;notification&lt;/code&gt; into the group.&lt;/p&gt;

&lt;p&gt;That escape hatch scales in two directions. Group broadcast isn't the only delivery mode: chanx can address a single connection, so a handler can hand heavy work to a real task queue (taskiq, Celery, ARQ) using &lt;code&gt;self.channel_name&lt;/code&gt; as a typed reply address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# consumer: queue the job with a reply address
&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;run_agent_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kiq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply_to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# worker process: send typed events back to that exact connection
&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;AgentConsumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JobProgressEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;...),&lt;/span&gt; &lt;span class="n"&gt;reply_to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# consumer: typed, routed, and documented like everything else
&lt;/span&gt;&lt;span class="nd"&gt;@event_handler&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_job_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;JobProgressEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProgressMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ProgressMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer stays a thin protocol adapter while the LLM work happens in a process built for it. And since the channel layer is just Redis, and chanx speaks the same API on Django Channels and FastAPI, the socket and the agent don't even have to share a service. A split I run in production: Django owns the user-facing WebSocket (sessions, ORM auth, a consumer that's little more than &lt;code&gt;passthrough_events&lt;/code&gt;) while a separate FastAPI service runs the models and broadcasts into the same conversation groups. Django never awaits an LLM; the agent service never touches a session. The contract is the only thing they share, and that's the part AsyncAPI documents.&lt;/p&gt;

&lt;p&gt;The demo runs on an in-memory layer with zero dependencies; set &lt;code&gt;REDIS_URL&lt;/code&gt; and the same code spans processes and instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human-in-the-loop: the approval round trip
&lt;/h2&gt;

&lt;p&gt;This is the part WebSockets were made for. The full sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model calls &lt;code&gt;delete_task&lt;/code&gt; → the run ends with &lt;code&gt;DeferredToolRequests&lt;/code&gt;, and the harness announces the paused calls as &lt;code&gt;tool_call&lt;/code&gt; messages, so the UI shows the tool card immediately (status: &lt;em&gt;awaiting approval&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;The server sends &lt;code&gt;approval_request&lt;/code&gt;, persists the message history, and simply stops. No polling, no hanging request&lt;/li&gt;
&lt;li&gt;The user clicks Approve or Deny → client sends &lt;code&gt;tool_decision&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The harness builds &lt;code&gt;DeferredToolResults&lt;/code&gt; and &lt;strong&gt;resumes the run&lt;/strong&gt; with the saved history:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ToolDecision&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;approvals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ToolApproved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;override_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;override_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;approved&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;ToolDenied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Denied by the user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;PENDING_APPROVALS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DeferredToolResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approvals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;approvals&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resumed run is just another &lt;code&gt;run_stream()&lt;/code&gt; call: approved tools execute (their events stream through the same handler), denied tools return the denial message to the model, and the model streams a final response either way. &lt;code&gt;ToolApproved(override_args=...)&lt;/code&gt; is a lovely detail: the user can &lt;em&gt;edit&lt;/em&gt; the arguments as part of approving, which is how real review UIs should work.&lt;/p&gt;

&lt;p&gt;Approvals also survive reconnects, in both directions. Pending requests are keyed by conversation rather than connection, and the consumer re-sends the open &lt;code&gt;approval_request&lt;/code&gt; to every new connection, so a refresh mid-approval gets a working approval card instead of a dead end. The E2E test literally approves from a second browser tab. One UI subtlety falls out of multi-tab: when a run resumes, other tabs still showing an open approval card mark it "resolved from another session" on the next &lt;code&gt;stream_end&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The gotcha: tell the model the harness has it covered
&lt;/h3&gt;

&lt;p&gt;My first instructions said "destructive operations require user approval". The model took that personally. Instead of calling &lt;code&gt;delete_task&lt;/code&gt;, it &lt;em&gt;asked in chat&lt;/em&gt;: "This is permanent, do I have your approval? (yes/no)". Technically obedient, completely bypassing the typed approval flow.&lt;/p&gt;

&lt;p&gt;The fix is to be explicit about the division of labor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When the user asks for a destructive operation, call the tool directly — do NOT
ask for confirmation in chat. The system automatically pauses destructive tool
calls and asks the user for approval in the UI.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you build approval flows, this prompt bug will find you. The model needs to know that safety is &lt;em&gt;infrastructure&lt;/em&gt;, not its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  AsyncAPI → TypeScript: the client writes itself
&lt;/h2&gt;

&lt;p&gt;chanx serves the contract at &lt;code&gt;/asyncapi.json&lt;/code&gt;; every payload above appears in &lt;code&gt;components.schemas&lt;/code&gt; as plain JSON Schema. A ~100-line script fetches the spec, collects the client-bound and server-bound messages from the AsyncAPI &lt;em&gt;operations&lt;/em&gt; (inputs vs replies), and feeds them to &lt;code&gt;json-schema-to-typescript&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// generated - src/generated/messages.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ToolCallMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolCallPayload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ClientMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;ToolDecisionMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ServerMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* union of all twelve server messages */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One codegen detail worth knowing: Pydantic marks &lt;code&gt;action&lt;/code&gt; as optional in JSON Schema (it has a default), but it's the discriminator, so the script forces it into &lt;code&gt;required&lt;/code&gt; for TypeScript to narrow on. With that, the entire client state layer is a single exhaustive switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ServerMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ChatState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text_delta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;// append to the streaming bubble&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_call&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;// add a tool card&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approval_request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;// render Approve/Deny&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suggestions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;// show follow-up chips&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream_end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="c1"&gt;// finalize + count tokens&lt;/span&gt;
    &lt;span class="c1"&gt;// TypeScript errors if any server message is unhandled&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rename a field in a Pydantic payload, run &lt;code&gt;pnpm generate&lt;/code&gt;, and &lt;code&gt;tsc&lt;/code&gt; lists every client line that just became wrong. Same loop as schema-first REST: the transport changed, the discipline didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistence: history as a typed artifact
&lt;/h2&gt;

&lt;p&gt;pydantic-ai ships &lt;code&gt;ModelMessagesTypeAdapter&lt;/code&gt;, a &lt;code&gt;TypeAdapter&lt;/code&gt; for full message histories, so conversation state is just another validated blob: &lt;code&gt;stream.all_messages_json()&lt;/code&gt; on the way out, &lt;code&gt;ModelMessagesTypeAdapter.validate_json(raw)&lt;/code&gt; on the way back in. Two functions, no bespoke serialization.&lt;/p&gt;

&lt;p&gt;On connect, the consumer replays the conversation as a &lt;code&gt;history&lt;/code&gt; message plus the current task list (&lt;code&gt;tasks_updated&lt;/code&gt;) and the latest follow-up chips. The transcript builder walks the typed history and reconstructs the &lt;em&gt;interleaving&lt;/em&gt;: user text, assistant text, and tool cards in order, each card carrying a &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;denied&lt;/code&gt;, or &lt;code&gt;awaiting&lt;/code&gt; status, so a denied deletion still renders as denied after a refresh. Since everything is stored per conversation, a sidebar with &lt;code&gt;/conversations&lt;/code&gt; (titles derived from the first prompt) and conversation delete falls out almost for free. Multi-turn context, reconnects, and resume-after-approval all sit on these same two functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the whole flow without an LLM
&lt;/h2&gt;

&lt;p&gt;The part I care most about: the entire WebSocket protocol (streaming, tool execution, approval, denial, history replay) runs in pytest with &lt;strong&gt;no API key&lt;/strong&gt;. pydantic-ai's &lt;code&gt;FunctionModel&lt;/code&gt; plays the model; a stream function scripts exactly what "the LLM" does. One wrinkle of structured output: the final answer is itself a call to the output tool, so the script emits that too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_task_flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tool_return&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ToolReturnPart&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_return&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# After the tool ran, answer via the structured-output tool
&lt;/span&gt;        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DeltaToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Result: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_return&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DeltaToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delete_task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 1}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;chanx's &lt;code&gt;WebsocketCommunicator&lt;/code&gt; drives the consumer, and the assertions read like the protocol spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ChatPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Delete task 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;replies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_all_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approval_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# the prompt echoes to the group, the paused call is announced, the run pauses
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;replies&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approval_request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;assert &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conv-delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;     &lt;span class="c1"&gt;# nothing deleted yet
&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolDecisionMessage&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="n"&gt;approved&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;...))&lt;/span&gt;
&lt;span class="n"&gt;replies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;comm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_all_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream_end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conv-delete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;       &lt;span class="c1"&gt;# now it's gone
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the suite covers the deny path, &lt;strong&gt;two tabs receiving identical broadcast feeds&lt;/strong&gt;, &lt;strong&gt;approving from a fresh connection after a disconnect&lt;/strong&gt;, the concurrent-run guard, transcript replay with denied and awaiting tool cards, and a contract test that pulls &lt;code&gt;/asyncapi.json&lt;/code&gt; and asserts every message type is present, so the schema the frontend generates from can't silently lose a message. Twenty tests, a couple of seconds, no LLM.&lt;/p&gt;

&lt;p&gt;One trap worth sharing: &lt;code&gt;Agent.override(model=...)&lt;/code&gt; uses contextvars, which don't propagate into the consumer's task under the test communicator. The factory pattern sidesteps it: tests call &lt;code&gt;build_agent(FunctionModel(...))&lt;/code&gt; and patch the consumer's agent directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production notes
&lt;/h2&gt;

&lt;p&gt;The demo makes three simplifications you'd revisit for production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scaling out&lt;/strong&gt;: the repo ships a &lt;code&gt;docker-compose.yml&lt;/code&gt; for Redis, and the README walks through the two-instance demo (chat on one port, watch the same stream on another).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt;: the consumer trusts the &lt;code&gt;conversation&lt;/code&gt; query param. chanx has an authentication hook (&lt;code&gt;post_authentication&lt;/code&gt;) where you'd validate a session or token before joining the group and replaying history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable pending approvals&lt;/strong&gt;: pending &lt;code&gt;DeferredToolRequests&lt;/code&gt; live in an in-process dict; store them in the database if approvals must survive restarts. And if you ever accept message history &lt;em&gt;from&lt;/em&gt; the client, run it through pydantic-ai's &lt;code&gt;sanitize_messages&lt;/code&gt; first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets are the natural transport for agents with approvals&lt;/strong&gt;: pause and resume are just messages, in both directions, on one connection. pydantic-ai's &lt;code&gt;run_stream()&lt;/code&gt; + &lt;code&gt;DeferredToolRequests&lt;/code&gt;/&lt;code&gt;DeferredToolResults&lt;/code&gt; hand you streaming and human-in-the-loop as library features; the harness adapting them to a transport is ~150 lines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output streams too&lt;/strong&gt;: partial-validation snapshots diff into text deltas, and extra fields like follow-up suggestions ride along in the same schema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the WebSocket protocol as a contract&lt;/strong&gt;: Pydantic models with discriminated actions on the server, AsyncAPI in the middle, generated TypeScript on the client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run in the background, broadcast to a group&lt;/strong&gt;: refresh-proof streams, multi-tab sync, approve-from-anywhere, and server-initiated notifications all come from one decision; events go to the conversation, not the socket. The same layer lets you offload runs to a worker or put a Django socket in front of a FastAPI agent&lt;/li&gt;
&lt;li&gt;Tell the model that approval is handled by infrastructure, or it will improvise its own confirmation dialog in prose&lt;/li&gt;
&lt;li&gt;Script the model with &lt;code&gt;FunctionModel&lt;/code&gt; and test the entire flow offline; deterministic agent tests are entirely achievable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this saves you some of the pain it took me to learn. If you try the pattern (or have your own tricks for agent WebSockets), the repo issues are open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Companion repo: &lt;a href="https://github.com/huynguyengl99/pydantic-ai-ws-agent" rel="noopener noreferrer"&gt;huynguyengl99/pydantic-ai-ws-agent&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pydantic.dev/docs/ai/" rel="noopener noreferrer"&gt;Pydantic AI docs&lt;/a&gt; · &lt;a href="https://pydantic.dev/docs/ai/tools-toolsets/deferred-tools/" rel="noopener noreferrer"&gt;deferred tools&lt;/a&gt; · &lt;a href="https://pydantic.dev/docs/ai/testing/" rel="noopener noreferrer"&gt;testing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/huynguyengl99/chanx" rel="noopener noreferrer"&gt;chanx&lt;/a&gt; - typed WebSockets for Django and FastAPI (&lt;a href="https://huynguyengl99.github.io/posts/chanx-structured-websockets-django-fastapi/" rel="noopener noreferrer"&gt;my intro post&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huynguyengl99.github.io/posts/schema-first-api-development-drf-react/" rel="noopener noreferrer"&gt;Schema-first API development with DRF + React&lt;/a&gt; - the same philosophy for REST&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>fastapi</category>
      <category>webdev</category>
    </item>
    <item>
      <title>🚀 Supercharge Your FastAPI WebSockets with Channel Layers &amp; Group Messaging</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Thu, 18 Sep 2025 02:38:11 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/supercharge-your-fastapi-websockets-with-channel-layers-group-messaging-19kb</link>
      <guid>https://dev.to/huynguyengl99/supercharge-your-fastapi-websockets-with-channel-layers-group-messaging-19kb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvl9ejtloufsvls5rnuf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvl9ejtloufsvls5rnuf2.png" alt=" " width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building real-time features in FastAPI? Hit the wall trying to send messages from HTTP endpoints to WebSocket clients? Struggling with group messaging and scalability?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast Channels&lt;/strong&gt; brings Django Channels' battle-tested architecture to FastAPI and the entire ASGI ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pain Points We've All Hit
&lt;/h2&gt;

&lt;p&gt;FastAPI's native WebSocket support gets you started, but quickly becomes limiting:&lt;/p&gt;

&lt;p&gt;❌ &lt;strong&gt;No group messaging&lt;/strong&gt; - can't broadcast to multiple connections&lt;br&gt;&lt;br&gt;
❌ &lt;strong&gt;No cross-process communication&lt;/strong&gt; - HTTP endpoints can't reach WebSocket clients&lt;br&gt;&lt;br&gt;
❌ &lt;strong&gt;Manual connection management&lt;/strong&gt; - tracking users, rooms, cleanup&lt;br&gt;&lt;br&gt;
❌ &lt;strong&gt;No scalability&lt;/strong&gt; - everything breaks with multiple server instances&lt;br&gt;&lt;br&gt;
❌ &lt;strong&gt;Testing nightmare&lt;/strong&gt; - WebSocket unit tests are painful to write  &lt;/p&gt;
&lt;h2&gt;
  
  
  What Fast Channels Delivers
&lt;/h2&gt;

&lt;p&gt;✅ &lt;strong&gt;Group messaging&lt;/strong&gt; - broadcast to chat rooms, user segments, notification groups&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Cross-process communication&lt;/strong&gt; - HTTP → WebSocket, background workers → clients&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Auto connection management&lt;/strong&gt; - join/leave groups automatically&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Multi-instance scaling&lt;/strong&gt; - Redis-backed message routing&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Built-in testing framework&lt;/strong&gt; - proper WebSocket test utilities&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Consumer patterns&lt;/strong&gt; - WebSocket handlers that feel like FastAPI views  &lt;/p&gt;
&lt;h2&gt;
  
  
  Battle-Tested in Production
&lt;/h2&gt;

&lt;p&gt;This isn't experimental tech. Django Channels has powered real-time features for &lt;strong&gt;millions of users&lt;/strong&gt; across thousands of production applications for years. Fast Channels brings that same proven architecture to FastAPI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simple but powerful
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncWebsocketConsumer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_room&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Auto-join on connect
&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Broadcast to all users in the room
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_room&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text_data&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production-Ready Features
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Redis Integration&lt;/strong&gt;: Full Redis support with Sentinel for high availability&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Multiple Backends&lt;/strong&gt;: In-memory for testing, Redis Queue for reliability, Redis Pub/Sub for speed&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Type Safety&lt;/strong&gt;: Complete mypy and pyright support&lt;br&gt;&lt;br&gt;
&lt;strong&gt;ASGI Universal&lt;/strong&gt;: Works with FastAPI, Starlette, Quart, any ASGI framework&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Testing Built-in&lt;/strong&gt;: Comprehensive WebSocket testing utilities  &lt;/p&gt;

&lt;h2&gt;
  
  
  FastAPI vs Fast Channels vs Alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Native FastAPI&lt;/th&gt;
&lt;th&gt;Broadcaster&lt;/th&gt;
&lt;th&gt;Fast Channels&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Basic WebSocket&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group messaging&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer patterns&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message persistence&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing framework&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto connection mgmt&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-process msgs&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production battle-tested&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Real-World Impact
&lt;/h2&gt;

&lt;p&gt;Teams are using Fast Channels for:&lt;/p&gt;

&lt;p&gt;🏪 &lt;strong&gt;E-commerce&lt;/strong&gt;: Live inventory, order tracking, flash sale updates&lt;br&gt;&lt;br&gt;
💬 &lt;strong&gt;Chat platforms&lt;/strong&gt;: Real-time messaging, typing indicators, presence&lt;br&gt;&lt;br&gt;
📊 &lt;strong&gt;Dashboards&lt;/strong&gt;: Live metrics, system monitoring, alerts&lt;br&gt;&lt;br&gt;
🎮 &lt;strong&gt;Gaming&lt;/strong&gt;: Multiplayer sync, leaderboards, real-time events&lt;br&gt;&lt;br&gt;
🔔 &lt;strong&gt;Notifications&lt;/strong&gt;: Push alerts, system updates, user notifications  &lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fast-channels[redis]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register a channel layer, create a consumer, add to FastAPI - done. Your WebSockets now scale across multiple processes and servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;Real-time features shouldn't require reinventing messaging infrastructure. Fast Channels gives you Django's proven patterns with FastAPI's developer experience - the best of both worlds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production-ready&lt;/strong&gt;: Redis Sentinel support, proper error handling, connection cleanup&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Developer-friendly&lt;/strong&gt;: Type safety, testing utilities, clear documentation&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Scalable&lt;/strong&gt;: Multi-process, multi-server, battle-tested architecture  &lt;/p&gt;

&lt;p&gt;Stop building WebSocket boilerplate. Start building features.&lt;/p&gt;




&lt;p&gt;🔗 &lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/huynguyengl99/fast-channels" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/fast-channels/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fast-channels.readthedocs.io/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fast-channels.readthedocs.io/en/latest/tutorial/index.html" rel="noopener noreferrer"&gt;Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Building real-time features? What's your biggest WebSocket challenge? 👇&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>websocket</category>
    </item>
    <item>
      <title>Building a Modern, Type-Safe Authentication System for Django REST Framework</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Wed, 09 Jul 2025 13:59:32 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/building-a-modern-type-safe-authentication-system-for-django-rest-framework-c42</link>
      <guid>https://dev.to/huynguyengl99/building-a-modern-type-safe-authentication-system-for-django-rest-framework-c42</guid>
      <description>&lt;p&gt;Have you ever spent hours fighting with authentication setup in your Django REST API? Or struggled with type errors that could have been caught at development time? Or wished your API documentation would just... work automatically?&lt;/p&gt;

&lt;p&gt;I've been there. After years of wrestling with existing Django authentication packages, I decided to build something better: &lt;strong&gt;DRF Auth Kit&lt;/strong&gt; – a modern, type-safe authentication toolkit that just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Authentication Shouldn't Be This Hard
&lt;/h2&gt;

&lt;p&gt;Let me paint a picture. You're building a modern web app with a Django REST API backend. You need:&lt;/p&gt;

&lt;p&gt;✅ JWT authentication with cookie support&lt;/p&gt;

&lt;p&gt;✅ Multi-factor authentication&lt;/p&gt;

&lt;p&gt;✅ Social login (Google, GitHub, etc.)&lt;/p&gt;

&lt;p&gt;✅ Automatic API documentation&lt;/p&gt;

&lt;p&gt;✅ Type safety throughout&lt;/p&gt;

&lt;p&gt;With existing solutions, you'd typically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install 3-4 different packages&lt;/li&gt;
&lt;li&gt;Write custom serializers and views&lt;/li&gt;
&lt;li&gt;Manually configure complex URL patterns&lt;/li&gt;
&lt;li&gt;Struggle with type hints and documentation&lt;/li&gt;
&lt;li&gt;Debug cryptic authentication errors&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;There had to be a better way.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: DRF Auth Kit
&lt;/h2&gt;

&lt;p&gt;DRF Auth Kit is what I wish existed when I started building Django APIs. Here's what makes it different:&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 One Package, Everything Included
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get everything you need&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;drf-auth-kit[all]

&lt;span class="c"&gt;# Or pick what you need&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;drf-auth-kit[mfa]     &lt;span class="c"&gt;# + Multi-factor auth&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;drf-auth-kit[social]  &lt;span class="c"&gt;# + Social authentication&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🔒 Universal Authentication Backend
&lt;/h3&gt;

&lt;p&gt;Instead of juggling multiple authentication classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;REST_FRAMEWORK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DEFAULT_AUTHENTICATION_CLASSES&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.authentication.AuthKitAuthentication&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;AUTH_KIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTH_TYPE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;jwt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# or 'token', 'custom'
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;USE_AUTH_COOKIE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;USE_MFA&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single configuration handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT tokens with automatic refresh&lt;/li&gt;
&lt;li&gt;DRF token authentication&lt;/li&gt;
&lt;li&gt;Custom authentication backends&lt;/li&gt;
&lt;li&gt;HTTP-only cookies with security headers&lt;/li&gt;
&lt;li&gt;Multi-factor authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛠️ Intelligent URL Management
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Conditional URL patterns&lt;/strong&gt; - URLs automatically included/excluded based on settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# When USE_MFA=True, automatically creates:
# POST /api/auth/login/         # First step (password)
# POST /api/auth/login/verify/  # Second step (MFA code)
# POST /api/auth/login/change-method/
# POST /api/auth/login/resend/
# GET /api/auth/mfa/           # MFA management endpoints
&lt;/span&gt;
&lt;span class="c1"&gt;# When USE_MFA=False, creates:
# POST /api/auth/login/         # Direct login
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Social authentication&lt;/strong&gt; - automatic endpoint generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Just add a provider to INSTALLED_APPS:
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allauth.socialaccount.providers.google&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.social&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Automatically creates:
# POST /api/auth/social/google/
# POST /api/auth/social/google/connect/
# GET /api/auth/social/accounts/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📝 Type Safety That Actually Works
&lt;/h3&gt;

&lt;p&gt;Every component includes comprehensive type hints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;auth_kit.authentication&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthKitAuthentication&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;auth_kit.serializers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LoginRequestSerializer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;auth_kit.views&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LoginView&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Full mypy and pyright compatibility&lt;/strong&gt; with comprehensive type hints throughout the codebase. Your IDE will thank you. No more guessing what parameters a function expects or what a setting does.&lt;/p&gt;

&lt;h3&gt;
  
  
  📚 API Documentation That Updates Itself
&lt;/h3&gt;

&lt;p&gt;Integration with DRF Spectacular means your API docs are always current:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Zero configuration needed
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;drf_spectacular&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;/api/docs/&lt;/code&gt; and you'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interactive Swagger UI&lt;/li&gt;
&lt;li&gt;Complete request/response schemas&lt;/li&gt;
&lt;li&gt;Authentication flow examples&lt;/li&gt;
&lt;li&gt;Working test interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpf0r3jm4h4v1fjvckcho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpf0r3jm4h4v1fjvckcho.png" alt="API Documentation Example" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🛡️ MFA Made Simple
&lt;/h3&gt;

&lt;p&gt;Multi-factor authentication that doesn't require a PhD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Enable MFA
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.mfa&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Just add this!
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;AUTH_KIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;USE_MFA&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# URLs automatically reconfigure
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Automatic URL reconfiguration&lt;/strong&gt; when MFA is enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login endpoint becomes two-step flow&lt;/li&gt;
&lt;li&gt;MFA management endpoints automatically created&lt;/li&gt;
&lt;li&gt;All endpoints documented in your OpenAPI schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email-based MFA with HTML/text templates&lt;/li&gt;
&lt;li&gt;Authenticator app support (Google Authenticator, Authy)&lt;/li&gt;
&lt;li&gt;Backup codes for account recovery&lt;/li&gt;
&lt;li&gt;Extensible system for custom MFA methods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The login flow becomes a clean two-step process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Username/password → Get ephemeral token&lt;/li&gt;
&lt;li&gt;MFA code → Get final auth tokens&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🌐 Social Authentication That Just Works
&lt;/h3&gt;

&lt;p&gt;Social login with 50+ providers and &lt;strong&gt;automatic URL generation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.social&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Automatic URL generation
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allauth.socialaccount.providers.google&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allauth.socialaccount.providers.github&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Add any provider, URLs created automatically!
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# URLs are automatically created for each provider:
# POST /api/auth/social/google/
# POST /api/auth/social/github/
# POST /api/auth/social/google/connect/
# POST /api/auth/social/github/connect/
# GET /api/auth/social/accounts/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No manual URL configuration needed!&lt;/strong&gt; The system scans your installed providers and creates endpoints automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  🍪 Security-First Cookie Authentication
&lt;/h3&gt;

&lt;p&gt;HTTP-only cookies with all the security features:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;AUTH_KIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;USE_AUTH_COOKIE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTH_COOKIE_SECURE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# HTTPS only
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTH_COOKIE_HTTPONLY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# No JavaScript access
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTH_COOKIE_SAMESITE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Lax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# CSRF protection
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect for modern web apps where you want security without complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: Complete Auth Setup
&lt;/h2&gt;

&lt;p&gt;Here's how you'd set up a complete authentication system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rest_framework&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.mfa&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.social&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allauth&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allauth.account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allauth.socialaccount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;allauth.socialaccount.providers.google&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;REST_FRAMEWORK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DEFAULT_AUTHENTICATION_CLASSES&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.authentication.AuthKitAuthentication&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;AUTH_KIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTH_TYPE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;jwt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;USE_AUTH_COOKIE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;USE_MFA&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Social auth providers
&lt;/span&gt;&lt;span class="n"&gt;SOCIALACCOUNT_PROVIDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;google&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SCOPE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AUTH_PARAMS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;access_type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;online&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OAUTH_PKCE_ENABLED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;APP&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-google-client-id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;secret&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your-google-client-secret&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# urls.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;api/auth/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.urls&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;api/auth/social/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.social.urls&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! You now have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT authentication with cookies&lt;/li&gt;
&lt;li&gt;User registration and email verification&lt;/li&gt;
&lt;li&gt;Password reset functionality&lt;/li&gt;
&lt;li&gt;Multi-factor authentication&lt;/li&gt;
&lt;li&gt;Social login with Google&lt;/li&gt;
&lt;li&gt;Complete API documentation&lt;/li&gt;
&lt;li&gt;Type safety throughout&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Customization: Make It Yours
&lt;/h2&gt;

&lt;p&gt;Need custom behavior? Override any component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Custom login serializer
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomLoginSerializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoginRequestSerializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;remember_me&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BooleanField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Your custom logic here
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;

&lt;span class="c1"&gt;# Apply it
&lt;/span&gt;&lt;span class="n"&gt;AUTH_KIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LOGIN_REQUEST_SERIALIZER&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp.serializers.CustomLoginSerializer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want a custom MFA method? Easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SMSMFAHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseMFAHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Your SMS implementation
&lt;/span&gt;        &lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your verification code: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Register it
&lt;/span&gt;&lt;span class="n"&gt;AUTH_KIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MFA_HANDLERS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.mfa.handlers.app.MFAAppHandler&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auth_kit.mfa.handlers.email.MFAEmailHandler&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;myapp.handlers.SMSMFAHandler&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Production-Ready Features
&lt;/h2&gt;

&lt;p&gt;DRF Auth Kit isn't just for prototypes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Comprehensive test coverage&lt;/strong&gt; (&amp;gt;95%) across multiple Python/Django versions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internationalization&lt;/strong&gt; support for 57 languages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security best practices&lt;/strong&gt; built-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance optimized&lt;/strong&gt; with efficient database queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; and &lt;strong&gt;audit logging&lt;/strong&gt; (coming soon)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Numbers Don't Lie
&lt;/h2&gt;

&lt;p&gt;Here's how DRF Auth Kit compares to existing solutions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;dj-rest-auth&lt;/th&gt;
&lt;th&gt;django-trench&lt;/th&gt;
&lt;th&gt;DRF Auth Kit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type Safety&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Full mypy/pyright&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto OpenAPI&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Complete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookie Auth&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Enhanced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MFA Support&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ Simplified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social Auth&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Better integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Auth&lt;/td&gt;
&lt;td&gt;⚠️ Complex&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ Easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I18n Support&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ 57 languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test Coverage&lt;/td&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;td&gt;✅ &amp;gt;95%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try It Today
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;drf-auth-kit[all]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the &lt;a href="https://drf-auth-kit.readthedocs.io/" rel="noopener noreferrer"&gt;complete documentation&lt;/a&gt; for detailed guides, examples, and API reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;The roadmap includes exciting features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebAuthn support&lt;/strong&gt; for passwordless authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware security keys&lt;/strong&gt; (YubiKey, FIDO2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; and &lt;strong&gt;audit logging&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced security features&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Join the Community
&lt;/h2&gt;

&lt;p&gt;DRF Auth Kit is open source and built by developers, for developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/forthecraft/drf-auth-kit" rel="noopener noreferrer"&gt;github.com/forthecraft/drf-auth-kit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: &lt;a href="https://drf-auth-kit.readthedocs.io/" rel="noopener noreferrer"&gt;drf-auth-kit.readthedocs.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/drf-auth-kit/" rel="noopener noreferrer"&gt;pypi.org/project/drf-auth-kit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building authentication shouldn't be a painful experience. With DRF Auth Kit, you get a modern, type-safe solution that grows with your project – from simple JWT authentication to complex multi-factor flows.&lt;/p&gt;

&lt;p&gt;The combination of type safety, automatic documentation, and developer-friendly APIs makes it perfect for both new projects and existing ones looking to modernize their authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are you waiting for?&lt;/strong&gt; Give it a try in your next project and let me know what you think! 🚀&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>authentication</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Chanx: The Missing Toolkit for Django Channels - Production-Ready WebSockets Made Simple</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Wed, 11 Jun 2025 08:13:18 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/chanx-the-missing-toolkit-for-django-channels-production-ready-websockets-made-simple-27om</link>
      <guid>https://dev.to/huynguyengl99/chanx-the-missing-toolkit-for-django-channels-production-ready-websockets-made-simple-27om</guid>
      <description>&lt;p&gt;&lt;em&gt;Building WebSocket applications with Django Channels just got a whole lot easier&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Django Channels
&lt;/h2&gt;

&lt;p&gt;Don't get me wrong - Django Channels is fantastic for adding WebSocket support to Django applications. But after building several real-time apps, I kept running into the same challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up authentication for WebSocket connections&lt;/li&gt;
&lt;li&gt;Validating message structures and preventing runtime errors&lt;/li&gt;
&lt;li&gt;Testing multi-user WebSocket interactions&lt;/li&gt;
&lt;li&gt;Managing group messaging patterns&lt;/li&gt;
&lt;li&gt;Debugging WebSocket connections during development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I found myself writing the same boilerplate code over and over. So I built &lt;strong&gt;Chanx&lt;/strong&gt; to solve these problems once and for all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Chanx?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/huynguyengl99/chanx" rel="noopener noreferrer"&gt;Chanx&lt;/a&gt; is a comprehensive toolkit that extends Django Channels with production-ready features. Think of it as what Django REST Framework is to Django views - but for WebSocket consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features That'll Save You Hours
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔐 DRF-Style Authentication
&lt;/h3&gt;

&lt;p&gt;Use your existing authentication classes with WebSockets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.generic.websocket&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncJsonWebsocketConsumer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework.authentication&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SessionAuthentication&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework.permissions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IsAuthenticated&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncJsonWebsocketConsumer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;authentication_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SessionAuthentication&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;permission_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IsAuthenticated&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Only authenticated users reach here
&lt;/span&gt;        &lt;span class="c1"&gt;# self.user is automatically populated
&lt;/span&gt;        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📝 Type-Safe Messaging with Pydantic
&lt;/h3&gt;

&lt;p&gt;No more runtime errors from malformed messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.messages.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseMessage&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;

&lt;span class="c1"&gt;# Union type for type safety
&lt;/span&gt;&lt;span class="n"&gt;AllMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatMessage&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;NotificationMessage&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncJsonWebsocketConsumer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AllMessages&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AllMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Pattern matching with full type safety
&lt;/span&gt;        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;NotificationMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🎮 WebSocket Playground
&lt;/h3&gt;

&lt;p&gt;Like Swagger, but for WebSockets! Auto-discovers your endpoints and lets you test them interactively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chanx.playground&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# urls.py
&lt;/span&gt;&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;playground/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chanx.playground.urls&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;/playground/websocket/&lt;/code&gt; and test your WebSocket endpoints without writing JavaScript!&lt;/p&gt;

&lt;h3&gt;
  
  
  👥 Simplified Group Messaging
&lt;/h3&gt;

&lt;p&gt;Pub/sub messaging made easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncJsonWebsocketConsumer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_groups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;room_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url_route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kwargs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;room_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_room_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;room_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Broadcast to all users in the room
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_group_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Messages automatically include metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello everyone!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_mine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🧪 Testing That Actually Works
&lt;/h3&gt;

&lt;p&gt;Multi-user WebSocket testing without the headaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.testing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebsocketTestCase&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebsocketTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ws_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ws/chat/room1/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_multi_user_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# First user (automatic setup)
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_authenticated_status_ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Second user with different credentials
&lt;/span&gt;        &lt;span class="n"&gt;user2_comm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_communicator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user2_headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user2_comm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Test group messaging
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user2_comm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_all_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_mine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Example: Building a Chat App
&lt;/h2&gt;

&lt;p&gt;Here's how simple it is to build a production-ready chat application:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Define Your Messages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.messages.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BaseGroupMessage&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserJoinedMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseGroupMessage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_joined&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_joined&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;

&lt;span class="n"&gt;ChatMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatMessage&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;PingMessage&lt;/span&gt;  &lt;span class="c1"&gt;# Include ping for health checks
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create Your Consumer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.generic.websocket&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncJsonWebsocketConsumer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.messages.incoming&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PingMessage&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.messages.outgoing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PongMessage&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncJsonWebsocketConsumer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ChatMessages&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;authentication_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SessionAuthentication&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;permission_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IsAuthenticated&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_groups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;room_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url_route&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kwargs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;room_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_room_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;room_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_authentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Called after successful authentication&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_group_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;UserJoinedMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ChatMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;PingMessage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PongMessage&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="c1"&gt;# Save to database and broadcast
&lt;/span&gt;                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_group_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Set Up Routing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# chat/routing.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.routing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;channels.routing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;URLRouter&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URLRouter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;int:room_id&amp;gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ChatConsumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_asgi&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# project/routing.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.routing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;channels.routing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;URLRouter&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URLRouter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ws/chat/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat.routing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Test It
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatRoomTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebsocketTestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ws_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ws/chat/123/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_user_can_join_and_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_authenticated_status_ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Should receive user_joined message
&lt;/span&gt;        &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_all_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_joined&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Send a chat message
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_communicator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_all_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Developers Love Chanx
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Complete Type Safety
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Generic type parameters for compile-time checking
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncJsonWebsocketConsumer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;IncomingMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Required: Your message types
&lt;/span&gt;    &lt;span class="n"&gt;ChannelEvents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Optional: Channel layer events
&lt;/span&gt;    &lt;span class="n"&gt;Room&lt;/span&gt;              &lt;span class="c1"&gt;# Optional: Model for object permissions
&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;queryset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# For object-level permissions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enhanced Routing
&lt;/h3&gt;

&lt;p&gt;Django-style routing specifically designed for WebSockets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chanx.routing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include&lt;/span&gt;

&lt;span class="c1"&gt;# Use familiar Django patterns
&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URLRouter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;room/&amp;lt;str:room_name&amp;gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ChatConsumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_asgi&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="nf"&gt;re_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^admin/(?P&amp;lt;id&amp;gt;\d+)/$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AdminConsumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_asgi&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api.routing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Channel Events
&lt;/h3&gt;

&lt;p&gt;Type-safe communication between consumers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Send events from views, tasks, or other consumers
&lt;/span&gt;&lt;span class="n"&gt;ChatConsumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_channel_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_room_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;NotificationEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;System update&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Handle in consumer
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NotificationEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;NotificationEvent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SystemMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;chanx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Essential links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📚 &lt;a href="https://chanx.readthedocs.io/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; - Comprehensive guides and examples&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/huynguyengl99/chanx" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; - Source code and issues&lt;/li&gt;
&lt;li&gt;🚀 &lt;a href="https://github.com/huynguyengl99/chanx-example" rel="noopener noreferrer"&gt;Complete Example&lt;/a&gt; - Production-ready chat app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quick setup:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# settings.py
&lt;/span&gt;&lt;span class="n"&gt;INSTALLED_APPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;channels&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rest_framework&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chanx.playground&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# For WebSocket testing UI
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;CHANX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SEND_COMPLETION&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Important for testing
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SEND_AUTHENTICATION_MESSAGE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Perfect for AI Applications
&lt;/h2&gt;

&lt;p&gt;Chanx is especially powerful for AI chatbots and streaming applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_ai_chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Stream AI response token by token&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ai_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AIStreamingMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Community and Feedback
&lt;/h2&gt;

&lt;p&gt;Chanx is actively maintained and used in production applications. The community is growing, and I'm always looking for feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What challenges are you facing with Django Channels?&lt;/li&gt;
&lt;li&gt;Which features would be most valuable to you?&lt;/li&gt;
&lt;li&gt;How can we make WebSocket development even easier?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Django Channels is powerful, but building production WebSocket applications shouldn't require reinventing the wheel every time. Chanx provides the missing pieces:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Authentication&lt;/strong&gt; - DRF integration out of the box&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Type Safety&lt;/strong&gt; - Catch errors at development time&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Testing&lt;/strong&gt; - Multi-user scenarios made simple&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Developer Tools&lt;/strong&gt; - WebSocket playground for debugging&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Group Messaging&lt;/strong&gt; - Pub/sub patterns simplified&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Production Ready&lt;/strong&gt; - Battle-tested in real applications  &lt;/p&gt;

&lt;p&gt;Whether you're building a chat application, real-time collaboration tool, or AI-powered interface, Chanx helps you ship faster with fewer bugs.&lt;/p&gt;

&lt;p&gt;Try it out and let me know what you think! 🚀&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your experience with Django Channels? Have you run into similar challenges? Share your thoughts in the comments!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>websockets</category>
      <category>python</category>
      <category>chatbot</category>
    </item>
    <item>
      <title>🚀 Streamline Your Python &amp; Django Development with Production-Ready Cookiecutter Templates</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Mon, 09 Jun 2025 15:18:53 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/streamline-your-python-django-development-with-production-ready-cookiecutter-templates-455o</link>
      <guid>https://dev.to/huynguyengl99/streamline-your-python-django-development-with-production-ready-cookiecutter-templates-455o</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I've created a comprehensive cookiecutter template collection that generates production-ready Python apps, PyPI packages, Django REST APIs, and DRF packages with modern tooling, best practices, and zero configuration hassle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Starting Projects Shouldn't Be This Hard
&lt;/h2&gt;

&lt;p&gt;How many times have you started a new Python project and spent hours setting up the same boilerplate code? Configuring &lt;code&gt;pyproject.toml&lt;/code&gt;, setting up pre-commit hooks, writing GitHub Actions, configuring Docker, setting up Django with all the modern best practices... &lt;/p&gt;

&lt;p&gt;The list goes on. And then you realize you forgot to configure type checking, or your test coverage setup is broken, or you're missing that one crucial piece of tooling that makes development smooth.&lt;/p&gt;

&lt;p&gt;I've been there. We've all been there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: One Template to Rule Them All (Actually, Four)
&lt;/h2&gt;

&lt;p&gt;That's why I created &lt;strong&gt;&lt;a href="https://github.com/huynguyengl99/cookiecutter-python" rel="noopener noreferrer"&gt;cookiecutter-python&lt;/a&gt;&lt;/strong&gt; — a curated collection of four specialized cookiecutter templates that cover the most common Python development scenarios:&lt;/p&gt;

&lt;h3&gt;
  
  
  🐍 &lt;strong&gt;Python App&lt;/strong&gt; - For General-Purpose Applications
&lt;/h3&gt;

&lt;p&gt;Perfect for CLI tools, data processing scripts, or any standalone Python application.&lt;/p&gt;

&lt;h3&gt;
  
  
  📦 &lt;strong&gt;Python Package&lt;/strong&gt; - For PyPI Publishing
&lt;/h3&gt;

&lt;p&gt;Ready-to-publish package structure with documentation, CI/CD, and multiple license options.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 &lt;strong&gt;Django REST App&lt;/strong&gt; - For Full-Stack Web APIs
&lt;/h3&gt;

&lt;p&gt;Complete Django REST Framework setup with authentication, database, optional Celery/WebSockets, and cloud deployment support.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧩 &lt;strong&gt;DRF Package&lt;/strong&gt; - For Reusable Django Components
&lt;/h3&gt;

&lt;p&gt;Lightweight structure for building and distributing Django REST Framework extensions.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes These Templates Special?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✨ &lt;strong&gt;Modern Tooling Out of the Box&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each template is carefully configured with specialized tooling for its use case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔧 Universal Tools (All Templates):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/astral-sh/uv" rel="noopener noreferrer"&gt;UV&lt;/a&gt;&lt;/strong&gt; for lightning-fast dependency management with intelligent dependency groups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/astral-sh/ruff" rel="noopener noreferrer"&gt;Ruff&lt;/a&gt;&lt;/strong&gt; for blazing-fast linting and formatting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/psf/black" rel="noopener noreferrer"&gt;Black&lt;/a&gt;&lt;/strong&gt; for consistent code formatting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual type checking&lt;/strong&gt; with both &lt;strong&gt;&lt;a href="https://mypy.readthedocs.io/" rel="noopener noreferrer"&gt;mypy&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://github.com/microsoft/pyright" rel="noopener noreferrer"&gt;pyright&lt;/a&gt;&lt;/strong&gt; for maximum type safety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-commit hooks&lt;/strong&gt; for automated quality checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest&lt;/strong&gt; with parallel execution (&lt;code&gt;pytest-xdist&lt;/code&gt;) and coverage reporting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; CI/CD pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;📦 Package Templates (PyPackage &amp;amp; DRF Package) Add:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://commitizen-tools.github.io/commitizen/" rel="noopener noreferrer"&gt;Commitizen&lt;/a&gt;&lt;/strong&gt; for conventional commits and automated versioning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://interrogate.readthedocs.io/" rel="noopener noreferrer"&gt;Interrogate&lt;/a&gt;&lt;/strong&gt; for docstring coverage monitoring with badge generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.sphinx-doc.org/" rel="noopener noreferrer"&gt;Sphinx&lt;/a&gt;&lt;/strong&gt; ecosystem for professional documentation (myst-parser, ReadTheDocs theme)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated publishing&lt;/strong&gt; workflows to PyPI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🌐 Django Templates (DRF &amp;amp; DRF Package) Add:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://factoryboy.readthedocs.io/" rel="noopener noreferrer"&gt;Factory Boy&lt;/a&gt;&lt;/strong&gt; for realistic test data generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://faker.readthedocs.io/" rel="noopener noreferrer"&gt;Faker&lt;/a&gt;&lt;/strong&gt; for generating fake data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/typeddjango/django-stubs" rel="noopener noreferrer"&gt;django-stubs&lt;/a&gt;&lt;/strong&gt; + &lt;strong&gt;&lt;a href="https://github.com/typeddjango/djangorestframework-stubs" rel="noopener noreferrer"&gt;djangorestframework-stubs&lt;/a&gt;&lt;/strong&gt; for comprehensive Django type checking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://django-debug-toolbar.readthedocs.io/" rel="noopener noreferrer"&gt;django-debug-toolbar&lt;/a&gt;&lt;/strong&gt; for development debugging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ipython.org/" rel="noopener noreferrer"&gt;IPython&lt;/a&gt;&lt;/strong&gt; for enhanced Django shell experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/spulec/freezegun" rel="noopener noreferrer"&gt;Freezegun&lt;/a&gt;&lt;/strong&gt; for time-based testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🏗️ &lt;strong&gt;Production-Ready Architecture&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The Django templates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; integration with connection pooling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; support for Celery and WebSockets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT authentication&lt;/strong&gt; with django-rest-auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API documentation&lt;/strong&gt; with Swagger/ReDoc&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; containerization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud storage&lt;/strong&gt; support (AWS S3, GCP, Azure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email providers&lt;/strong&gt; integration (SendGrid, Mailgun, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎯 &lt;strong&gt;Zero Configuration Philosophy&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each generated project is immediately runnable. No additional setup required. Just generate, install dependencies, and start developing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;First, you'll need to install cookiecutter. I recommend using Homebrew (macOS) or pipx for a clean installation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS (Recommended):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;cookiecutter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux/Windows with pipx:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;cookiecutter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Alternative (with pip):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; cookiecutter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not just &lt;code&gt;pip install cookiecutter&lt;/code&gt;?&lt;/strong&gt; Since we'll be using UV for Python dependency management in the generated projects, keeping cookiecutter isolated prevents potential conflicts and keeps your global Python environment clean.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Generate Your Project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cookiecutter gh:huynguyengl99/cookiecutter-python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll be prompted to select your template type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Select a template
1 - Python app (A cookiecutter template for a normal python app)
2 - Python package (A cookiecutter template for a python package)  
3 - DRF (A cookiecutter template for a django rest framework app)
4 - DRF package (A cookiecutter template for a drf package)
Choose from [1/2/3/4] (1):
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start Developing Immediately
&lt;/h3&gt;

&lt;p&gt;For a &lt;strong&gt;Django REST App&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
uv &lt;span class="nb"&gt;sync&lt;/span&gt;                    &lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;       &lt;span class="c"&gt;# Start PostgreSQL/Redis&lt;/span&gt;
scripts/manage.sh migrate  &lt;span class="c"&gt;# Run migrations&lt;/span&gt;
scripts/manage.sh runserver &lt;span class="c"&gt;# Start development server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your API documentation is immediately available at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Swagger UI: &lt;code&gt;http://localhost:8000/api/schema/swg/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ReDoc: &lt;code&gt;http://localhost:8000/api/schema/redoc/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Deep Dive: What Each Template Provides
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🐍 Python App Template
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: CLI tools, data processing, automation scripts&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Includes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modern &lt;code&gt;pyproject.toml&lt;/code&gt; configuration&lt;/li&gt;
&lt;li&gt;Docker setup for easy deployment&lt;/li&gt;
&lt;li&gt;Comprehensive testing framework&lt;/li&gt;
&lt;li&gt;Code quality tools (Ruff, Black, mypy, pyright)&lt;/li&gt;
&lt;li&gt;GitHub Actions for CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Generated Structure&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-app/
├── pyproject.toml
├── docker-compose.yml
├── Dockerfile
├── scripts/
│   └── lint.sh
└── my_app/
    └── __init__.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📦 Python Package Template
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Open-source libraries, reusable components&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Includes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PyPI-ready project structure&lt;/li&gt;
&lt;li&gt;Sphinx documentation with ReadTheDocs integration&lt;/li&gt;
&lt;li&gt;Multiple license options (MIT, BSD, Apache, GPL)&lt;/li&gt;
&lt;li&gt;Automated versioning with Commitizen&lt;/li&gt;
&lt;li&gt;Publishing workflow to PyPI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Generated Structure&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-package/
├── pyproject.toml
├── LICENSE
├── docs/
├── tests/
├── .github/workflows/
└── my_package/
    └── __init__.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🌐 Django REST App Template
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Web APIs, SaaS backends, microservices&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration Options&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: PostgreSQL with multiple version options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: JWT with optional Google OAuth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Tasks&lt;/strong&gt;: Optional Celery integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time&lt;/strong&gt;: Optional WebSocket support with Channels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Storage&lt;/strong&gt;: AWS S3, Google Cloud, Azure support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email&lt;/strong&gt;: 10+ provider integrations (SendGrid, Mailgun, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Style&lt;/strong&gt;: Optional camelCase conversion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Generated Structure&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-api/
├── pyproject.toml
├── docker-compose.yml
├── Dockerfile
├── scripts/
│   └── manage.sh
└── my_api/
    ├── config/
    │   ├── settings/
    │   ├── urls.py
    │   └── wsgi.py
    ├── accounts/        # Custom user model
    ├── core/           # Utilities
    └── status/         # Health check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom User Model&lt;/strong&gt; with email authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT Authentication&lt;/strong&gt; with refresh token rotation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Versioning&lt;/strong&gt; ready structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comprehensive Testing&lt;/strong&gt; with factories and fixtures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Interface&lt;/strong&gt; customization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Logging&lt;/strong&gt; with structured logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health Checks&lt;/strong&gt; and monitoring endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧩 DRF Package Template
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Perfect for&lt;/strong&gt;: Django REST Framework extensions, reusable API components&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Includes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sandbox Django project for testing&lt;/li&gt;
&lt;li&gt;Documentation with Django integration&lt;/li&gt;
&lt;li&gt;Package publishing to PyPI&lt;/li&gt;
&lt;li&gt;Testing across multiple Django/DRF versions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Unique Feature&lt;/strong&gt;: The sandbox directory serves dual purposes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Testing Environment&lt;/strong&gt;: Run tests against your package&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Playground&lt;/strong&gt;: Manual testing with a real Django app&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Advanced Features That Set These Apart
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔧 &lt;strong&gt;Intelligent Dependency Management with UV&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each template uses UV's dependency groups for optimal development experience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependency-groups]&lt;/span&gt;
&lt;span class="py"&gt;dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"commitizen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"factory-boy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pytest-django"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c"&gt;# Core development&lt;/span&gt;
&lt;span class="py"&gt;lint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ruff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mypy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pyright"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;           &lt;span class="c"&gt;# Code quality  &lt;/span&gt;
&lt;span class="py"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"pytest-cov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pytest-xdist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"coverage"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="c"&gt;# Testing tools&lt;/span&gt;
&lt;span class="py"&gt;docs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"sphinx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"myst-parser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"interrogate"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="c"&gt;# Documentation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Smart defaults&lt;/strong&gt; with &lt;code&gt;uv sync&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application templates&lt;/strong&gt;: Auto-installs &lt;code&gt;dev&lt;/code&gt; + &lt;code&gt;lint&lt;/code&gt; groups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package templates&lt;/strong&gt;: Auto-installs all groups for comprehensive development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selective installation&lt;/strong&gt;: &lt;code&gt;uv sync --group test&lt;/code&gt; for specific workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛡️ &lt;strong&gt;Type Safety First&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Every template includes comprehensive type checking with dual tools for maximum coverage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mypy.readthedocs.io/" rel="noopener noreferrer"&gt;mypy&lt;/a&gt;&lt;/strong&gt; with specialized Django and DRF plugins for framework-specific type checking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/microsoft/pyright" rel="noopener noreferrer"&gt;pyright&lt;/a&gt;&lt;/strong&gt; for advanced static analysis and fast feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/typeddjango/django-stubs" rel="noopener noreferrer"&gt;django-stubs&lt;/a&gt;&lt;/strong&gt; for comprehensive Django typing (Django templates)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This dual approach catches different categories of type issues and provides the most robust type safety available in the Python ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  📊 &lt;strong&gt;Quality Metrics &amp;amp; Automation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Package templates&lt;/strong&gt; include sophisticated quality monitoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Docstring coverage with automatic badge generation&lt;/span&gt;
interrogate &lt;span class="nt"&gt;--generate-badge&lt;/span&gt; docs/_static/ my_package

&lt;span class="c"&gt;# Conventional commits with automated versioning&lt;/span&gt;
cz commit  &lt;span class="c"&gt;# Interactive commit creation&lt;/span&gt;
cz bump    &lt;span class="c"&gt;# Automatic version bumping and changelog&lt;/span&gt;

&lt;span class="c"&gt;# Parallel testing for faster feedback&lt;/span&gt;
pytest &lt;span class="nt"&gt;-n5&lt;/span&gt;  &lt;span class="c"&gt;# Runs tests across 5 parallel processes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quality gates enforce standards:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;80% docstring coverage&lt;/strong&gt; requirement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100% type hint coverage&lt;/strong&gt; with mypy strict mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero linting violations&lt;/strong&gt; with Ruff and Black&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conventional commit format&lt;/strong&gt; for automated changelog generation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⚡ &lt;strong&gt;Modern Python Practices&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PEP 621&lt;/strong&gt; compliant &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UV&lt;/strong&gt; for dependency management (10x faster than pip)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruff&lt;/strong&gt; for linting (100x faster than flake8)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.10+&lt;/strong&gt; with modern syntax support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🚀 &lt;strong&gt;Production Deployment Ready&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Django templates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage Docker builds&lt;/strong&gt; for optimized images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gunicorn&lt;/strong&gt; configuration for production WSGI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment-based settings&lt;/strong&gt; (dev/test/production)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static file handling&lt;/strong&gt; with WhiteNoise&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database connection pooling&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Usage Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario 1: Building a SaaS API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cookiecutter gh:huynguyengl99/cookiecutter-python
&lt;span class="c"&gt;# Select: 3 - DRF&lt;/span&gt;
&lt;span class="c"&gt;# Configure: PostgreSQL, JWT auth, Celery, SendGrid emails, AWS S3&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;my-saas-api
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
scripts/manage.sh migrate
scripts/manage.sh createsuperuser
scripts/manage.sh runserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Full-featured API with user authentication, background tasks, email integration, and cloud storage. Ready for production deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Publishing a Python Library
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cookiecutter gh:huynguyengl99/cookiecutter-python
&lt;span class="c"&gt;# Select: 2 - Python package&lt;/span&gt;
&lt;span class="c"&gt;# Configure: MIT license, GitHub username, PyPI publishing&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;my-library
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;pytest  &lt;span class="c"&gt;# Tests pass immediately&lt;/span&gt;
scripts/lint.sh  &lt;span class="c"&gt;# Code quality checks pass&lt;/span&gt;
cz commit  &lt;span class="c"&gt;# Conventional commits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Professional package structure ready for PyPI publishing with documentation, CI/CD, and automated releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: Building a DRF Extension
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cookiecutter gh:huynguyengl99/cookiecutter-python
&lt;span class="c"&gt;# Select: 4 - DRF package&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;my-drf-extension
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;  &lt;span class="c"&gt;# Test database&lt;/span&gt;
pytest sandbox  &lt;span class="c"&gt;# Test against real Django app&lt;/span&gt;
python sandbox/manage.py runserver  &lt;span class="c"&gt;# Manual testing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Professional DRF package with testing sandbox and publishing workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Best Practices Baked In
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧪 &lt;strong&gt;Testing Excellence&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each template includes sophisticated testing infrastructure:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🏃‍♂️ Performance-Optimized Testing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel execution&lt;/strong&gt; with &lt;code&gt;pytest-xdist&lt;/code&gt; (5 parallel processes by default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart test discovery&lt;/strong&gt; with proper Django integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage reporting&lt;/strong&gt; with branch coverage and XML output for CI integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🏭 Test Data Generation (Django Templates):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Factory Boy integration for realistic test data
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;accounts.factories&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UserFactory&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_user_creation&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;⏰ Time-Based Testing:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Freezegun for testing time-dependent code
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;freezegun&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;freeze_time&lt;/span&gt;

&lt;span class="nd"&gt;@freeze_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2023-01-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timestamp_behavior&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Test behavior at specific time
&lt;/span&gt;    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;🔧 Advanced Testing Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pytest-mock&lt;/strong&gt; for sophisticated mocking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pytest-django&lt;/strong&gt; for Django-specific fixtures and database handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Factory Boy&lt;/strong&gt; with custom factory metaclass for type-safe model factories&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage exclusions&lt;/strong&gt; for migrations, settings, and other non-testable code&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📝 &lt;strong&gt;Documentation Standards&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Package templates include professional documentation tooling:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📖 Sphinx Documentation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic API documentation&lt;/strong&gt; generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReadTheDocs&lt;/strong&gt; integration with automated builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MyST parser&lt;/strong&gt; for Markdown support in reStructuredText projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-references&lt;/strong&gt; and &lt;strong&gt;intersphinx&lt;/strong&gt; linking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;📊 Documentation Quality Monitoring:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://interrogate.readthedocs.io/" rel="noopener noreferrer"&gt;Interrogate&lt;/a&gt;&lt;/strong&gt; for docstring coverage monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic badge generation&lt;/strong&gt; showing documentation coverage percentage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80% coverage requirement&lt;/strong&gt; enforced in CI/CD&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG badges&lt;/strong&gt; integrated into documentation and README
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check docstring coverage&lt;/span&gt;
interrogate &lt;span class="nt"&gt;-vv&lt;/span&gt; my_package

&lt;span class="c"&gt;# Generate coverage badge&lt;/span&gt;
interrogate &lt;span class="nt"&gt;--generate-badge&lt;/span&gt; docs/_static/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;📋 API Documentation (Django Templates):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Swagger UI&lt;/strong&gt; and &lt;strong&gt;ReDoc&lt;/strong&gt; auto-generation from code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI 3.0&lt;/strong&gt; specification export&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive API testing&lt;/strong&gt; directly in documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔄 &lt;strong&gt;CI/CD Pipeline&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;GitHub Actions workflows include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧪 Comprehensive Testing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-version testing&lt;/strong&gt; across Python 3.10-3.13&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel test execution&lt;/strong&gt; with &lt;code&gt;pytest-xdist&lt;/code&gt; for faster feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage reporting&lt;/strong&gt; with Codecov integration and XML output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix testing&lt;/strong&gt; across multiple dependency versions (Django templates)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;✅ Code Quality Gates:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linting pipeline&lt;/strong&gt; with Ruff for import sorting and style checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual type checking&lt;/strong&gt; with both mypy and pyright&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code formatting&lt;/strong&gt; verification with Black&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TOML file validation&lt;/strong&gt; and sorting with toml-sort&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;📦 Automated Publishing (Package Templates):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic versioning&lt;/strong&gt; with Commitizen integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated PyPI publishing&lt;/strong&gt; on tagged releases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub releases&lt;/strong&gt; with automated changelog generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build artifact&lt;/strong&gt; caching for faster CI runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🔒 Security &amp;amp; Quality:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependency vulnerability scanning&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-commit hook validation&lt;/strong&gt; in CI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build reproducibility&lt;/strong&gt; with UV lock files&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎨 &lt;strong&gt;Code Quality&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Pre-commit hooks automatically run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ruff&lt;/strong&gt; for linting and import sorting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Black&lt;/strong&gt; for code formatting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toml-sort&lt;/strong&gt; for TOML file organization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type checking&lt;/strong&gt; with mypy/pyright&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Q: Why four separate templates instead of one configurable template?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;: Each template is optimized for its specific use case. A Python package has different needs than a Django API. Separate templates mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cleaner code&lt;/strong&gt; without excessive conditionals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster generation&lt;/strong&gt; with only relevant files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better maintenance&lt;/strong&gt; with focused concerns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier customization&lt;/strong&gt; for specific needs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Q: Why UV instead of pip/poetry?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;: UV is incredibly fast (10-100x faster than pip) and handles both Python version management and dependency resolution. It's the future of Python package management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: Can I customize the generated templates?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;: Absolutely! The templates are designed to be starting points. All configuration is in standard files (&lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;settings.py&lt;/code&gt;, etc.) that you can modify as needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q: What about database migrations in Django templates?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;: The Django templates include a custom user model and initial migrations. After generation, run &lt;code&gt;scripts/manage.sh migrate&lt;/code&gt; and you're ready to go.&lt;/p&gt;




&lt;h2&gt;
  
  
  Future Roadmap
&lt;/h2&gt;

&lt;p&gt;I'm continuously improving these templates based on community feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI template&lt;/strong&gt; for modern async APIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js + Django&lt;/strong&gt; full-stack template&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes deployment&lt;/strong&gt; configurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More cloud provider&lt;/strong&gt; integrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional authentication&lt;/strong&gt; providers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Getting Started Today
&lt;/h2&gt;

&lt;p&gt;Ready to streamline your Python development? Here's how to get started:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install cookiecutter&lt;/strong&gt;: &lt;code&gt;pip install cookiecutter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate your project&lt;/strong&gt;: &lt;code&gt;cookiecutter gh:huynguyengl99/cookiecutter-python&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose your template&lt;/strong&gt; and configure options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start developing&lt;/strong&gt; immediately!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Useful Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📂 Repository&lt;/strong&gt;: &lt;a href="https://github.com/huynguyengl99/cookiecutter-python" rel="noopener noreferrer"&gt;github.com/huynguyengl99/cookiecutter-python&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;📖 Documentation&lt;/strong&gt;: Full README with detailed setup instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🐛 Issues&lt;/strong&gt;: Report bugs or request features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;💡 Discussions&lt;/strong&gt;: Share your use cases and get help&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These cookiecutter templates represent hundreds of hours of research, testing, and refinement to create the ideal starting point for Python projects. Whether you're building a simple CLI tool or a complex Django API, you can focus on your core business logic instead of wrestling with configuration.&lt;/p&gt;

&lt;p&gt;The templates embody modern Python best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type safety&lt;/strong&gt; with comprehensive type checking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code quality&lt;/strong&gt; with automated formatting and linting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing excellence&lt;/strong&gt; with comprehensive test setups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production readiness&lt;/strong&gt; with Docker and deployment configs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer experience&lt;/strong&gt; with helpful scripts and documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give them a try for your next project. I'm confident they'll save you hours of setup time and help you ship better code faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are you waiting for? Let's build something amazing together!&lt;/strong&gt; 🚀&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you used these templates? Found them helpful? Have suggestions for improvements? I'd love to hear about your experience! Drop a comment below or open an issue on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>cookiecutter</category>
      <category>bestpractices</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Easy Multilingual Page (Scales to Over 50 Languages)</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Fri, 21 Jun 2024 14:17:13 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/easy-multilingual-page-scales-to-over-50-languages-3m4o</link>
      <guid>https://dev.to/huynguyengl99/easy-multilingual-page-scales-to-over-50-languages-3m4o</guid>
      <description>&lt;p&gt;Hi all,&lt;/p&gt;

&lt;p&gt;So, a few months back, I kicked off a project to create a multilingual website. Let me tell you, managing content in multiple languages is a real headache. It’s doable if you’re only dealing with 3 or 4 languages, but once you hit 30+, it’s a whole different ball game trying to keep everything updated and consistent.&lt;/p&gt;

&lt;p&gt;To make life easier, I decided to bring in a CMS for my Astro-based site. I dug into frameworks like Wagtail and Strapi since I’m familiar with Python and Node.js, but man, those were tough nuts to crack. Plus, I wanted a centralized place for all my content instead of having to jump between different pages for each language. This way, I can easily compare content across languages without getting lost.&lt;/p&gt;

&lt;p&gt;And here's the kicker: ChatGPT is shaping up to be a solid translator, but no one’s integrated it into any frameworks yet. So, with my Django and Astrowind know-how (Astrowind is this cool UI built on Astro and Tailwind, and it’s open source), I whipped up a small Django library/framework to act as a headless CMS. I also tweaked the Astrowind source to pull in content from the CMS with multilingual support.&lt;/p&gt;

&lt;p&gt;Check out this demo if you wanna see it in action:&lt;br&gt;
&lt;a href="https://django-astrowind.netlify.app/en"&gt;https://django-astrowind.netlify.app/en&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not much has changed from the original Astrowind (&lt;a href="https://astrowind.vercel.app/"&gt;https://astrowind.vercel.app/&lt;/a&gt;), except now you can switch languages on the fly, and the content is managed through a CMS with hashed caching.&lt;/p&gt;

&lt;p&gt;If you’re interested in using it, I’ve put together a tutorial to help you set it up on your own machine:&lt;br&gt;
&lt;a href="https://django-headless-cms.readthedocs.io/en/latest/quick-start.html"&gt;https://django-headless-cms.readthedocs.io/en/latest/quick-start.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hope you dig it. And I hope my little project can be of some help to you guys.&lt;/p&gt;

</description>
      <category>django</category>
      <category>astrojs</category>
      <category>multilingual</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Introducing Django Headless CMS: A Powerful and Flexible CMS Solution</title>
      <dc:creator>Huy Nguyen</dc:creator>
      <pubDate>Thu, 20 Jun 2024 06:39:52 +0000</pubDate>
      <link>https://dev.to/huynguyengl99/introducing-django-headless-cms-a-powerful-and-flexible-cms-solution-82d</link>
      <guid>https://dev.to/huynguyengl99/introducing-django-headless-cms-a-powerful-and-flexible-cms-solution-82d</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Are you looking for an effortless way to create and manage a headless CMS? Look no further! I'm excited to introduce &lt;strong&gt;Django Headless CMS&lt;/strong&gt;, a robust, open-source content management system designed to simplify the process of managing and delivering content across multiple platforms. Built on Django, this package leverages the sturdy foundation of the Django framework while providing a headless approach, making it ideal for modern web applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Choose Django Headless CMS?
&lt;/h2&gt;

&lt;p&gt;Why choose Django-headless-cms over alternatives like &lt;a href="https://wagtail.org/"&gt;Wagtail&lt;/a&gt;, &lt;a href="https://www.django-cms.org/"&gt;Django-CMS&lt;/a&gt;, &lt;a href="https://strapi.io/"&gt;Strapi&lt;/a&gt;, or &lt;a href="https://www.contentful.com/"&gt;Contentful&lt;/a&gt;?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headless CMS with minimal configuration&lt;/strong&gt;: Unlike Wagtail and Django-CMS, which are primarily headed CMS solutions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive UI&lt;/strong&gt;: Preferred over Strapi for its user interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python &amp;amp; Django-based&lt;/strong&gt;: Built on a robust framework with numerous extensions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Source&lt;/strong&gt;: Unlike Contentful and other paid services, it allows you to self-host your CMS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration&lt;/strong&gt;: Easily integrates with many existing Python and Django libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized multi-language support&lt;/strong&gt;: Reduces redundancy and allows different content across multiple languages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;p&gt;Django Headless CMS comes packed with features to enhance your content management experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema Migrations&lt;/strong&gt;: Manage content schema as database migrations, making it easier to sync from development to production environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioning Content&lt;/strong&gt;: Revert to any previously saved version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish/Draft Content&lt;/strong&gt;: Manage published and draft content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown Editor Support&lt;/strong&gt;: Enhanced content editing experience, useful for Posts/Articles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-language Support&lt;/strong&gt;: Even for Markdown fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto Translate/Force Re-translate&lt;/strong&gt;: Use ChatGPT or build your own translation interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recursive Actions&lt;/strong&gt;: Apply actions like Publish, Translate, and Force Re-translate to referenced objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized Queries&lt;/strong&gt;: Auto prefetch and select related queries for optimization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter Published Objects&lt;/strong&gt;: Easily filter to show only published content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto Admin&lt;/strong&gt;: Simplify admin page setup with features like:

&lt;ul&gt;
&lt;li&gt;Sortable inline Many-to-Many (M2M) relationships.&lt;/li&gt;
&lt;li&gt;Sortable generic inlines.&lt;/li&gt;
&lt;li&gt;Display publish status of child objects.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto Serializer&lt;/strong&gt;: Simplify serializer creation, including nested relations. Override specific serializers as needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rapid API Development&lt;/strong&gt;: Build APIs (Views/Viewsets) quickly and easily with auto serializers and optimized queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy Data Import/Export&lt;/strong&gt;: Use the admin interface or management commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-generated API Documentation &amp;amp; Playground&lt;/strong&gt;: Automatically generate API documentation and an interactive playground.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;To get started with Django Headless CMS, follow the tutorial from the docs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://django-headless-cms.readthedocs.io/en/latest/quick-start.html"&gt;Quick Start Guide&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Want to see a quick demo? By the end of this tutorial, you will have a CMS that can be used to build a multi-lingual page that looks like this: &lt;a href="https://django-astrowind.netlify.app/en"&gt;Django Astrowind Demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publish/Unpublish/Recursively publish:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwx3axvu100imt07zqds5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwx3axvu100imt07zqds5.png" alt="Image description" width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-language &amp;amp; markdown editor:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xvglf5zgt6099ou026h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xvglf5zgt6099ou026h.png" alt="Image description" width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F126c91f21w3dvv2nkeg9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F126c91f21w3dvv2nkeg9.png" alt="Image description" width="800" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Drag-to-sort related items:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftto50ju0e3cft38haukn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftto50ju0e3cft38haukn.png" alt="Image description" width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Auto-translate to multiple languages:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcm4evb8lcq38eti7nge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcm4evb8lcq38eti7nge.png" alt="Image description" width="800" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;And other cool features. Read more in the docs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;After experimenting with various CMS frameworks, I built Django Headless CMS and Django Astrowind to meet my specific needs and address issues faced by others in the community.&lt;/p&gt;

&lt;p&gt;For more information, explore the &lt;a href="https://django-headless-cms.readthedocs.io/"&gt;Django Headless CMS documentation&lt;/a&gt; and the &lt;a href="https://github.com/huynguyengl99/django-astrowind"&gt;Django Astrowind repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>django</category>
      <category>cms</category>
      <category>opensource</category>
      <category>python</category>
    </item>
  </channel>
</rss>
