Real-time chat with a Rust backend and a React + TypeScript client. WebSockets, JWT auth, room-based access control, and message persistence in PostgreSQL.
- About
- Capabilities
- Essential features
- Architecture
- How to use
- How to develop
- Tech stack
- Author
- License
live-chat-server is an intermediate Rust project that demonstrates asynchronous
networking end to end: WebSockets for live messaging, PostgreSQL for durable
storage, JWT authentication, and room-based access control — paired with a
React + TypeScript client that speaks a typed protocol to the server.
Users can register, log in, create or join chat rooms, exchange messages in real
time, react to messages, and see full history after reconnecting. The system is
built single-server first (in-memory room registry + broadcast channels);
setting REDIS_URL switches it to cross-instance Pub/Sub broadcasting.
A few design properties carry through the whole codebase:
- Save before broadcast — every message is persisted to PostgreSQL before it is delivered over WebSockets.
- Consistent identity — messages carry IDs and timestamps, so clients render history identically after every reconnect.
- Be defensive everywhere — message-size limits, input validation, and membership re-checks apply to every action, not just the happy path.
- Observable — structured
tracinglogs for connects, disconnects, errors, and broadcasts;/healthliveness probe.
Full HTTP + WebSocket protocol reference: docs/api.md.
Backend — Rust, Axum + Tokio
- Auth: register / login with argon2id hashing and JWT (HS256, 24 h)
bearer tokens;
/api/me. - Rooms: create, list, get, join (idempotent); open enrollment — a room name is
enough to join. Leaving stays on the API (
POST /api/rooms/{id}/leave) but is no longer surfaced in the UI. - History: paginated messages (
limit+beforecursor), newest first, members only. - Real-time chat: WebSocket with pre-upgrade auth + access checks, per-room broadcast, save-before-broadcast persistence, presence join/leave events, revoked-membership teardown, reactions, typing events, and read receipts.
- Storage: SQLx with compile-time-checked queries against PostgreSQL; migrations bundled and applied on startup.
- Optional Redis Pub/Sub fan-out when
REDIS_URLis set (graceful in-memory fallback otherwise).
Frontend — React 19 + TypeScript (Vite)
- Register / login with live username and password validation.
- Room list with search and unread-count badges; create and join rooms.
- Live chat: streaming messages, load-older pagination, reactions with an emoji picker, typing indicators, presence, and read receipts ("Seen by...").
- Resilient WebSocket client with reconnect + backoff; marks the room read when you return to the tab.
- A persisted light theme scoped to the conversation panel, the About page and
the auth screen — the app shell (topbar + room list) stays dark — with a
theme toggle on every screen. Plus a room-search shortcut (
Ctrl/Cmd + K), toasts, desktop notifications, keyboard navigation, and screen-reader labels. - Typed REST + WebSocket client with response normalization, 118 unit tests.
Every capture below comes from the same seeded two-user room (alice + bob), in the dark theme. Each block highlights one part of the app end to end.
Register or log in with live username and password validation that runs before the request is sent. Passwords are hashed with argon2id on the server and sessions use a JWT bearer token. A theme toggle and the About link sit in the top-right corner.
Search the room list, create a room, or join an existing one with a single click. Unread badges mark rooms with new messages, and each room shows who created it.
Messages stream over a single WebSocket and are persisted to PostgreSQL before they are broadcast, so they reappear after a reconnect. Sending advances a per-user read cursor, which is why other members see the "Seen by …" receipt under your last message.
Hover an incoming message to reveal the quick reaction bar. A reaction is stored and broadcast like any other event, and reactions already on a message show inline beside it.
The composer has an emoji picker for dropping an emoji into the draft without leaving the keyboard.
The server relays ephemeral typing events, so the room shows who is typing, and presence join/leave events keep the online list current.
A hash-routed About page (#/about) with a quick how-to, the stack, and links
to the repo and the API reference. It is reachable logged in or out, and carries
its own theme toggle.
flowchart LR
B["Browser<br/>(React + TypeScript)"]
B -->|"REST /api<br/>auth · rooms · history"| A["Rust API<br/>(Axum + Tokio)"]
B -->|"WebSocket /api/ws<br/>live message stream"| A
A -->|"save-before-broadcast"| DB[("PostgreSQL<br/>users · rooms · memberships · messages")]
A -->|"in-memory room registry<br/>+ broadcast channels"| A
A -.->|"REDIS_URL set →"| R[("optional Redis<br/>cross-instance Pub/Sub")]
The client only ever talks to the Vite dev server, which proxies /api
(HTTP + WebSocket) and /health to the backend — no CORS during development.
WebSocket frames are normalized by a typed client so the UI and the server
agree on optional fields even across server versions.
The easiest way to see it running is Docker Compose:
Base compose runs standalone on any OS. Host mounts (gh/opencode/gitconfig) merge only when a Linux ELF opencode binary exists. Native Windows uses the Python launcher.
| Platform | Start | In-container opencode | Notes |
|---|---|---|---|
| Linux | make dev-up |
Yes | Full support |
| macOS | make dev-up |
Host only (Mach-O can't run in Linux) | OK |
| WSL2 | make dev-up |
Yes (inside distro) | Full support |
| Windows (native) | python scripts/dev-up.py dev-up or .\scripts\dev-up.ps1 dev-up |
Host only | See Troubleshooting; CRLF guarded |
dev-up does not require opencode; only make opencode / make shell require a Linux ELF binary.
make dev-up # Postgres + API (:8083) + frontend (:5173)- Open http://localhost:5173.
- Register a username and password (any unique username works).
- Create a room — or ask a friend for a room name and join it.
- Chat: messages, emoji reactions, typing indicators, read receipts.
- Close the tab and come back: history is persisted in PostgreSQL — open the
room again and the client fetches it via
GET /api/rooms/{id}/messages.
Stop everything with make dev-down.
No Docker? Run the pieces directly — see How to develop.
- Rust 1.99+ (the mise toolchain pins
rust), and Node.js 24+ with npm — Node is not pinned by mise; use nvm, corepack, or your package manager - Docker + Docker Compose for
make dev-up, or a local PostgreSQL
make dev-up # full stack with one commandOr run each component by hand — backend first (it runs migrations on startup):
cd backend
cargo fetch
cargo run # reads DATABASE_URL + PORT (default 8080)cd frontend
npm ci
npm run dev # Vite on :5173, proxies /api to the backendThe Vite proxy forwards /api (HTTP + WebSocket) and /health to the
backend. It defaults to http://localhost:8083, which is where make dev-up
exposes the API. When running the backend by hand on its default port, point
the proxy at it:
cd frontend
VITE_API_PROXY_TARGET=http://localhost:8080 npm run dev| Command | What it does |
|---|---|
make check |
Every gate: backend fmt/clippy/test/build (--locked) + frontend format/lint/typecheck/test/build |
make dev-up / make dev-down |
Start / stop the Compose stack |
make db-clean |
Truncate all app data in the dev database (destructive) |
make screenshots |
Playwright capture harness (needs the stack running) |
cd backend && cargo test |
Backend unit + integration tests (WS, pub/sub, read receipts); needs a reachable DATABASE_URL |
cd backend && sqlx migrate run |
Apply migrations manually |
cd frontend && npm run test |
Frontend Vitest unit tests |
cd frontend && npm run build |
Type-check + production build |
Ports used by the Compose stack (override via .env):
| Service | Host | Container |
|---|---|---|
| API | 8083 | 8080 |
| Frontend | 5173 | 5173 |
| PostgreSQL | 5434 | 5432 |
| Redis (optional) | 6381 | 6379 |
The harness (frontend/scripts/screenshots.mjs) seeds a two-user room over the
public API and WebSocket — messages, reactions, a read receipt, typing — then
captures one PNG per feature plus the README hero (the eight images shown
above), all in the dark theme. It is not part of make check and CI never runs
it.
npm ci
npx playwright install chromium # one-time; on Linux usually needs --with-deps (requires sudo)
make dev-up # stack must be running
SCREENSHOTS_DIR=../docs/screenshots make screenshots # committed outputThe default output directory is frontend/screenshots/ (gitignored); the
published images live in docs/screenshots/ (committed) — hence the
SCREENSHOTS_DIR=../docs/screenshots override above, relative to the
frontend/ directory the harness runs in. VITE_ORIGIN points the browser
elsewhere if your stack is not on :5173.
| Layer | Choice |
|---|---|
| Backend | Rust — Axum 0.8 on Tokio, SQLx 0.9 (compile-time-checked SQL), Serde, tracing |
| Database | PostgreSQL 16, migrations with sqlx |
| Realtime | WebSockets, per-room broadcast; optional Redis Pub/Sub for multi-instance |
| Auth | argon2id password hashing, JWT (HS256) bearer tokens |
| Frontend | React 19 + TypeScript (Vite); Oxlint + Oxfmt; Vitest |
| Tooling | Docker Compose, mise, GitHub Actions CI (Rust + frontend jobs) |
Built by Felipe Inoue — a study and portfolio project demonstrating asynchronous networking, WebSockets, concurrency, authentication, and persistence. Feedback and questions are welcome — open an issue or reach out via the GitHub profile.
All rights reserved — this is a personal portfolio project, not open source. See LICENSE for the full notice.
Third-party dependencies (Axum, Tokio, SQLx, React, Vite, and others) retain
their own licenses; see backend/Cargo.lock and frontend/package-lock.json.







