Skip to content

About

Real-time chat with a Rust backend and a React + TypeScript client. WebSockets, JWT auth, room-based access control, and message persistence in PostgreSQL.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

live-chat-server logo — teal speech bubble

live-chat-server

Real-time chat with a Rust backend and a React + TypeScript client. WebSockets, JWT auth, room-based access control, and message persistence in PostgreSQL.

CI Rust Axum SQLx PostgreSQL React TypeScript WebSockets license

Live Chat — the room list on the left and a live conversation on the right (dark theme)


Table of contents

About

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 tracing logs for connects, disconnects, errors, and broadcasts; /health liveness probe.

Full HTTP + WebSocket protocol reference: docs/api.md.

Capabilities

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 + before cursor), 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_URL is 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.

Essential features

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.

Authentication

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.

Register / log in screen

Rooms

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.

Room list with unread badges

Live chat & read receipts

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.

Live chat with a Seen by bob read receipt

Reactions

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.

Hover reaction bar on an incoming message

Emoji picker

The composer has an emoji picker for dropping an emoji into the draft without leaving the keyboard.

Composer emoji picker open

Typing & presence

The server relays ephemeral typing events, so the room shows who is typing, and presence join/leave events keep the online list current.

Typing indicator showing bob is typing

About page

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.

About page

Architecture

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")]
Loading

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.

How to use

The easiest way to see it running is Docker Compose:

OS matrix (dev sandbox)

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)
  1. Open http://localhost:5173.
  2. Register a username and password (any unique username works).
  3. Create a room — or ask a friend for a room name and join it.
  4. Chat: messages, emoji reactions, typing indicators, read receipts.
  5. 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.

How to develop

Prerequisites

  • 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

Quick start

make dev-up         # full stack with one command

Or 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 backend

The 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

Commands

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

Regenerating the screenshots

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 output

The 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.

Tech stack

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)

Author

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.

License

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.


Back to top

About

Real-time chat with a Rust backend and a React + TypeScript client. WebSockets, JWT auth, room-based access control, and message persistence in PostgreSQL.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages