TL;DR: CrossChat is a real multi-room chat server: FastAPI + WebSockets, message history persisted in SQLite with replay for newcomers, room isolation, a small REST API, a minimal single-page frontend, a pytest suite that covers the real-time behavior, and Docker + Render deploy configs. Zero external services — no Firebase, no vendor lock-in, everything runs from the repo. github.com/Shivay00001/crosschat.
The problem: chat demos are either toys or traps
Real-time chat is one of those things that looks trivial and isn't. Most open-source examples fall into two camps: toy echo servers with no persistence (refresh the page and history is gone, one room, no tests), or full Firebase/Supabase builds that trade a weekend of setup for permanent vendor lock-in. What's missing is the middle: a self-hostable, real multi-room chat with persisted history, a clean protocol, and tests that prove the real-time behavior — small enough to read in an afternoon.
CrossChat is that middle. One Python backend, one SQLite file, one HTML frontend, and a test suite that actually connects WebSocket clients and asserts on message delivery.
What it does
| Endpoint | Behavior |
|---|---|
GET / | Single-page chat UI — pick a username, join or create rooms, live messages |
WS /ws/{room}/{username} | Join a room; newcomers get history replay, then live join / message / leave events |
GET /api/rooms | Rooms with message counts |
GET /api/rooms/{room}/history?limit=50 | Persisted history (limit capped at 200) |
GET /health | Health check for deploy platforms |
How it works: the WebSocket lifecycle
The core of the repo is main.py (~2.9 KB — genuinely readable end to end). A client connecting to /ws/{room}/{username} goes through this exact sequence:
- Validate. Room and username must match
^[A-Za-z0-9_-]{1,40}$; anything else gets a WebSocket close with code 1008 (policy violation). No anonymous rooms, no path-traversal room names, no 10 KB usernames. - Accept and register. The socket is accepted and added to the room's set under an
asyncio.Lock. - History replay. Before anything live happens, the newcomer receives
{"type": "history", "messages": [...]}— the room's persisted messages, oldest first. This is the detail toy chat servers skip, and it's what makes the app feel real. - Join broadcast. Everyone in the room (including the newcomer) gets a
joinevent. - Message loop. Each received text is stripped; empties are skipped; messages are capped at 2,000 characters, saved to SQLite via
db.save_message(), and broadcast to the room as{"type": "message", ...}including the database id and timestamp. - Disconnect. On
WebSocketDisconnect, the socket is removed under the lock and aleaveevent is broadcast.
Broadcast without leaks
broadcast() deserves a look: it snapshots the room's socket set under the lock, sends to each outside the lock (so one slow client can't block the others), collects dead sockets, and discards them under the lock afterward. That three-step pattern — snapshot, send, sweep — is the correct way to fan out over mutable connection sets, and it's easy to get wrong.
Persistence: one table, no ORM
db.py is refreshingly honest about its scope — its docstring is literally "SQLite message history. One table, no ORM." The schema is a single messages table (id, room, username, text, ts) with an index on (room, id) so history queries stay fast. init_db() runs CREATE TABLE IF NOT EXISTS at startup; the database path is overridable with the CROSSCHAT_DB environment variable (which is also how the tests point at a throwaway database).
The test suite proves the real-time behavior
tests/test_chat.py is the strongest signal that this repo is real rather than a sketch. Using FastAPI's TestClient, it:
- Connects two WebSocket clients to the same room and asserts each receives the other's messages with correct usernames.
- Verifies history persists across connections and that rooms are isolated (a message in
room1never appears inroom2's history, via both WebSocket replay and the REST endpoint). - Verifies newcomer history replay — a client joining later receives earlier messages.
- Asserts invalid room names are rejected (
/api/rooms/bad!name/history→ 400). - Asserts the frontend is served and contains the expected markers.
Run it with python -m pytest tests/ -q. Testing WebSocket fan-out, replay, and isolation — not just the REST layer — is what separates a demo from a component you could actually build on.
How to run it
git clone https://github.com/Shivay00001/crosschat.git
cd crosschat
pip install -r requirements.txt
uvicorn main:app --port 8005
# open http://localhost:8005 in two browser windows and chat
Deployment is covered three ways: a Dockerfile + docker-compose.yml for containers, a render.yaml for one-click Render deploys (free plan, Python 3.12.3, health check on /health, CROSSCHAT_DB env var), and GitHub Actions workflows for CI and hosting deploys.
One honest note on naming: the repo's GitHub description says "powered by Firebase Data Connect," but the tree implements FastAPI + WebSockets + SQLite — and the README states it explicitly: "No external services, no Firebase — everything runs from this repo." The firebase-hosting workflows are deploy targets, not dependencies. The code is the source of truth, and the code is Firebase-free.
Honest status and roadmap
Status: working. The server boots, the frontend chats, history persists and replays, rooms are isolated, and the test suite covers the behaviors that matter. This is a real, usable chat component — suitable for internal tools, prototypes, classrooms, and as a reference implementation.
What's not here is also clear: there's no authentication (anyone can claim any username), no message editing or deletion, no typing indicators, no pagination cursors for very long histories, and a single process holds all connections in memory — fine for teams and demos, not for thousands of concurrent rooms.
Roadmap (sensible next steps):
- Token-based auth so usernames are claimed, not just asserted.
- Room management API (create/delete/rename) instead of implicit room creation.
- Typing indicators and read receipts on the WebSocket protocol.
- Message editing/deletion with history consistency.
- Cursor-based pagination for long histories.
- Redis pub/sub fan-out so multiple worker processes can serve the same rooms.
- Rate limiting on connects and messages for production hardening.
Who should look at this repo
- Backend developers wanting a minimal-but-correct WebSocket fan-out pattern (snapshot → send → sweep) in FastAPI.
- Teams needing a self-hosted chat component without a Firebase/AWS bill or account.
- Students learning WebSockets — the whole server is under 3 KB and the tests show exactly what correct behavior looks like.
The frontend: small but complete
static/index.html (~5.9 KB) is a single-page app with no framework and no build step, and it covers the full chat UX: a login box for picking a username, a sidebar listing rooms with a "new room" input, a header showing the current room plus a live/offline status indicator, a message pane, and a composer. The JavaScript is careful in the ways that matter:
esc()HTML-escapes every rendered string — usernames and message text can't inject markup.- Join/leave events render as system messages, distinct from chat content.
- Your own messages get a
mestyle so they're visually distinct. - The status indicator flips between "live" and "offline" with the socket state, so a dropped connection is visible, not silent.
- Room creation is just typing a name and joining — rooms come into existence on first use, matching the server's implicit-creation model.
Protocol reference
The WebSocket protocol is small enough to document completely — a genuine virtue:
| Direction | Message | Shape |
|---|---|---|
| Server → client | History replay | {"type": "history", "messages": [{id, room, username, text, ts}, …]} |
| Server → client | Join / leave | {"type": "join"|"leave", "username": …, "text": "… joined …"} |
| Server → client | Chat message | {"type": "message", "id": …, "room": …, "username": …, "text": …, "ts": …} |
| Client → server | Chat message | Plain text (stripped; empty ignored; capped at 2,000 chars) |
Every server message is JSON; every client message is raw text. Timestamps are Unix floats from the server, ids are SQLite row ids — so clients can deduplicate and order deterministically. If you wanted to write a native mobile client or a bot, this table plus the REST endpoints are the entire integration surface.
Finally, note the dependency footprint: FastAPI, uvicorn, and the standard library. No ORM, no migration framework, no message broker, no external database. That minimalism is a deliberate architectural choice — every dependency you don't take is a failure mode you don't inherit — and it's what makes the repo deployable to a free Render instance in minutes.
Frequently asked questions
Does CrossChat need Firebase?
No. It runs entirely on FastAPI, WebSockets, and SQLite. The firebase-hosting workflows are deploy targets, not runtime dependencies — the README states "no external services, no Firebase."
How many users can it handle?
It's a single-process asyncio server: plenty for teams, classrooms, and prototypes. Multi-worker scale via Redis pub/sub is on the roadmap.
Where are messages stored?
In a SQLite file (crosschat.db by default), one table, no ORM. Override the path with the CROSSCHAT_DB environment variable.
Is there authentication?
Not yet — usernames are asserted by the client. Token-based auth is the first item on the roadmap.
The bottom line
CrossChat is what a reference implementation should be: small enough to read completely, correct enough to trust the patterns, and tested on the behaviors that actually matter for real-time systems — delivery, replay, isolation. If you need chat without a cloud bill, clone it; if you're learning WebSockets, read main.py first and the tests second.