The fastest way to demo an LLM is a print loop; the fastest way to ship one is a chat interface people can actually use β€” with history, streaming, file uploads, authentication, and a thread they can return to. Chainlit is the framework that gives you all of it from a single Python file: decorate a function with @cl.on_message, send a reply with cl.Message(...).send(), run chainlit run demo.py -w, and a full React chat application appears in your browser, wired to your code over Socket.IO. The quickstart is genuinely two commands β€” pip install chainlit and chainlit hello β€” and the same surface grows with your application: steps that render agent reasoning as nested spans, elements that put images, audio, video, PDFs, and Pyplot figures inside the conversation, settings widgets, action buttons, and OAuth logins. Behind the simplicity sits real infrastructure: a FastAPI server, a websocket layer with session state, a pluggable data layer for threads and users, and instrumentation that turns LangChain, LlamaIndex, and OpenAI calls into visible steps automatically. Since May 2025 the project is community-maintained, with a formal maintainer agreement steering releases.

The repository is a monorepo with two citizens. The backend is a Python package whose public face β€” the cl module β€” is a two-hundred-line assembly of decorators and primitives, backed by a seventeen-hundred-line server, a typed configuration system, per-framework instrumentation modules, and a data layer with SQLAlchemy and DynamoDB implementations. The frontend is a React and Vite application with Zustand state management, built once at install time and served as static assets by the Python server. The two talk over a Socket.IO channel whose event names β€” message, step, element, settings β€” mirror the Python primitives exactly, which is what makes the backend code read like a specification of the UI. As always in this series, what follows is an educational tour of published source code.

Chainlit overview architecture diagram

Chainlit at a glance: the CLI launches the FastAPI server with your script; the socket layer receives UI events, creates sessions, and invokes your decorated handlers; Messages, Steps, and Elements stream back through the emitter to the React frontend, while a data layer persists threads and users.

Reading the overview from left to right:

  • Everything you import lives in the chainlit module, a two-hundred-line namespace assembly.
  • The CLI runs your script with watch mode and scaffolds new apps.
  • The FastAPI server handles auth, static files, and project settings.
  • The socket layer is the bridge between browser events and your handlers.
  • Messages carry stream_token for token-by-token replies.
  • Steps render nested tool and agent traces.
  • Elements are the media system β€” Image, Audio, Video, Pdf, Pyplot, TaskList.
  • The data layer persists threads, steps, elements, and users.

Why You Need This

The first reason is that the programming model is the right abstraction for chat applications. The chainlit module exports decorators β€” on_message, on_chat_start, on_settings_update β€” and primitives that map one-to-one onto what users see. The Message class does the hard streaming work: stream_token accumulates tokens, flushes at a computed cadence, and updates one UI bubble for the whole reply instead of spamming the socket. The Step class gives tool calls and intermediate reasoning a nested visual home β€” the @cl.step(type="tool") decorator turns any function into a traceable span β€” and the Element hierarchy covers every artifact an agent produces, from plots to task lists. Underneath, the context module uses context variables so your handler code never touches a session ID, and user_session provides the per-user scratchpad every chatbot eventually needs.

The second reason is the transport and server layer, which is production-shaped rather than demo-shaped. The server at seventeen hundred lines carries the whole HTTP surface: OAuth logins through a provider registry spanning Azure AD, GitHub, Google, Okta, and Keycloak, JWT and header authentication for programmatic access, element file upload and retrieval, project settings endpoints, and the static serving of the built frontend. The socket module authenticates each websocket connection from its cookie or token, restores sessions across reconnects, and routes the window_message event into your handler with the session bound. The session and emitter pair up per connection β€” one holding state, the other pushing events β€” so concurrent users are isolated by construction. The config module types every setting from a config.toml, from CORS to markdown features.

The third reason is instrumentation and persistence, the two things that make a chatbot a product. The callbacks system hooks every lifecycle event, and on top of it sit automatic adapters: the OpenAI patcher turns SDK calls into streamed steps without any code changes, the LangChain handler translates runs and chains into Steps, and LlamaIndex, MistralAI, and Semantic Kernel each get their module β€” plus an MCP client that surfaces Model Context Protocol servers in the UI so tools attach mid-conversation. Persistence is a clean contract in BaseDataLayer implemented by the default layer over SQLAlchemy, DynamoDB, or Literal AI, with storage clients for S3, Azure, and GCS behind element files. The React frontend with its Zustand stores is fully themable and bundled into the package.

Chainlit detail architecture diagram

The detail view: entry points with CLI and typed config, the HTTP tier with OAuth and JWT, the realtime tier of sockets, sessions, contexts, and emitters, the primitives your code sends, framework instrumentation, the pluggable data layer with SQL and blob backends, and the React application receiving every event.

Walking the detail diagram, one user message travels the full stack. The browser’s React app sends the event to the socket layer, which authenticates from cookies or JWT, creates a session, and binds it into the context your handler reads. The handler appends to chat_context, maybe mutates user_session, and emits Messages, Steps, and Elements β€” each persisted through the data layer contract via the default implementation and its SQL or blob storage backends. If OpenAI or LangChain is in play, the instrumentation modules create Steps invisibly; if MCP servers are configured, the MCP client manages their tools per session. The server started it all through the CLI reading typed config, and state lands in the chat store.

From Install to Your First Chat

Two commands, one file. pip install chainlit then chainlit hello verifies the whole stack by opening a demo app in your browser. Your own app is a Python file: decorate async def main(message: cl.Message) with @cl.on_message, reply with await cl.Message(content="...").send(), and run chainlit run demo.py -w β€” the -w watch mode hot-reloads on save. Streaming replies come from msg = cl.Message(content=""); await msg.send() followed by await msg.stream_token(token) in your generation loop. Authentication is configuration, not code: set [auth] OAuth credentials in config.toml or run with header auth, and every conversation is owned by a persisted user. The data layer activates with a database URL, giving you thread history and resumable sessions; without one, chat state lives in memory. For agent frameworks, the adapters are opt-in imports β€” import litellm, LangChain callback setup, or the OpenAI patcher β€” and Steps appear in the UI without you writing UI code. Theme everything through config.toml, brand it with custom CSS, and deploy as any ASGI app behind your reverse proxy.

Honest limits: Chainlit gives you a chat-shaped application, and apps that outgrow that shape hit its frame. Multi-tenant production deployments need to think hard about the data layer β€” the default persistence is straightforward but not sharded, and very high concurrency pushes the single-process Socket.IO model toward multiple workers with sticky sessions. The community-maintained status means release cadence and review bandwidth now depend on volunteer maintainers, and some framework adapters lag their upstream APIs. The frontend ships as a built bundle, so deep UI customization means forking the React app rather than styling alone. Streaming assumes one assistant bubble per handler call, so exotic multi-message flows need manual sequencing. And auto-instrumentation, while magical, adds overhead and can surface steps you would rather hide β€” every adapter allows filtering, but you have to find the switches. For what it aims at β€” turning a Python function into a credible chat product in an afternoon β€” nothing else matches the time-to-demo.

Watch PyShine on YouTube

Contents