Skip to content

About

Full-stack real-time chat — STOMP/WebSocket messaging, JWT auth with refresh-token rotation, group chats, file sharing, online presence, Docker Compose

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Realtime Chat Hub

A production-grade, full-stack realtime chat application — Spring Boot backend, React + Vite frontend, PostgreSQL, JWT auth with refresh-token rotation, and WebSocket (STOMP) messaging end to end.

Single-repo monolith: backend/ (Spring Boot 3 · Java 21 · Maven) and frontend/ (React 18 · Vite · Tailwind). Ships with a complete docker-compose stack including PostgreSQL and a mailpit SMTP catcher.


Highlights

  • Auth — register / login / refresh-with-rotation / logout / forgot+reset password, BCrypt(12) hashing, role-based (USER · ADMIN), CORS hardened, rate-limited.
  • Realtime messaging — 1:1 and group chats, typing indicators, read receipts, online presence, browser + audible notifications.
  • Rich messages — text, emoji, image and file attachments (15 MB cap), edit, soft-delete, pin / unpin, message search across your rooms, pinned-message strip.
  • Groups — create, rename, add / remove members, leave, member roles (OWNER / ADMIN / MEMBER).
  • Profile — display name, bio, avatar upload, last-seen, online dot, search the directory.
  • Admin panel — system stats, paginated user list, suspend / re-activate accounts, change roles (with self-action guard).
  • Polished UI — distinct dark / light themes (jade-green accent on warm paper / deep ink surfaces), refined typography (Bricolage Grotesque + Hanken Grotesk), subtle film-grain texture, responsive down to mobile.
  • Docs — Swagger UI for the entire REST API.
  • DevOps — multi-stage Dockerfiles, docker-compose with health-checks, persistent volumes for Postgres and uploaded files, .env.example for all knobs.

Tech stack

Layer Choice
Backend Java 21 · Spring Boot 3.3 · Spring Security · Spring Data JPA · Spring WebSocket (STOMP + SockJS) · Spring Mail · Spring Boot Actuator
Auth JJWT 0.12 (HS256 access tokens) · opaque DB-backed refresh tokens with rotation
Database PostgreSQL 16
Docs springdoc-openapi (Swagger UI)
Frontend React 18 · Vite 5 · React Router 6 · Axios · @stomp/stompjs · sockjs-client · Tailwind CSS 3
Build / Run Maven · npm · Docker · docker compose · nginx (frontend reverse proxy)

Repository layout

realtime-chat-hub/
├── backend/                    Spring Boot service (com.chathub)
│   ├── pom.xml
│   ├── Dockerfile              multi-stage Maven → JRE 21
│   └── src/main/
│       ├── java/com/chathub/
│       │   ├── ChatHubApplication.java
│       │   ├── config/         AppProperties · SecurityConfig · WebSocketConfig …
│       │   ├── controller/     7 REST controllers (auth, users, chats, messages, …)
│       │   ├── dto/            request/response records
│       │   ├── entity/         User · ChatRoom · Message · …
│       │   ├── exception/      typed exceptions + global handler
│       │   ├── repository/     Spring Data JPA repositories
│       │   ├── security/       JWT filter · UserDetails · SecurityUtils
│       │   ├── service/        business logic
│       │   └── websocket/      STOMP auth interceptor · presence listener · controller
│       └── resources/application.yml
├── frontend/                   React + Vite app
│   ├── package.json
│   ├── vite.config.js
│   ├── tailwind.config.js
│   ├── nginx.conf              SPA + reverse proxy used by the Docker image
│   ├── Dockerfile              multi-stage Node → nginx
│   └── src/
│       ├── api/                axios client + per-resource modules
│       ├── components/         Sidebar · ChatWindow · MessageBubble · modals · icons …
│       ├── context/            Auth · Chat · Theme · Toast providers
│       ├── hooks/              useStomp (STOMP-over-SockJS lifecycle)
│       ├── pages/              Login · Register · ChatPage · AdminPage · …
│       ├── utils/              constants · formatting helpers
│       ├── App.jsx
│       └── main.jsx
├── docker-compose.yml
├── .env.example
└── README.md

Quick start — Docker (recommended)

Prerequisites: Docker 24+ and Docker Compose v2.

git clone <this-repo> realtime-chat-hub
cd realtime-chat-hub
cp .env.example .env
docker compose up --build

When all three containers report healthy:

Service URL
Web UI http://localhost:5173
API (direct) http://localhost:8080
Swagger UI http://localhost:5173/swagger-ui.html
Mailpit inbox http://localhost:8025
Postgres localhost:5432 (chathub / chathub)

Seeded accounts (non-prod profile)

Username Password Role
admin Admin@12345 ADMIN
alice Alice@12345 USER
bob Bob@12345 USER

Set SPRING_PROFILES_ACTIVE=prod in .env to seed only the admin account.


Local development (without Docker)

Backend

Requirements: JDK 21, Maven 3.9+, a running PostgreSQL 16 with database chathub and user chathub/chathub. Optionally a local SMTP catcher on localhost:1025 (e.g. docker run -p 1025:1025 -p 8025:8025 axllent/mailpit).

cd backend
export DB_HOST=localhost DB_PORT=5432
export DB_NAME=chathub DB_USERNAME=chathub DB_PASSWORD=chathub
mvn spring-boot:run

The first launch creates all tables (spring.jpa.hibernate.ddl-auto=update) and seeds the demo accounts. Swagger UI is at http://localhost:8080/swagger-ui.html.

Frontend

Requirements: Node 20+.

cd frontend
cp .env.example .env        # optional — defaults are fine
npm install
npm run dev                 # http://localhost:5173 with Vite proxy → backend

The Vite dev server proxies /api, /ws and /uploads to http://localhost:8080 (override with VITE_PROXY_TARGET).


Environment variables

All variables have safe defaults; only DB_PASSWORD and JWT_SECRET should be changed in production.

Variable Default Purpose
DB_HOST / DB_PORT / DB_NAME / DB_USERNAME / DB_PASSWORD postgres / 5432 / chathub / chathub / chathub PostgreSQL connection
JWT_SECRET (dev secret) Base64-encoded HMAC-SHA secret for access tokens
JWT_ACCESS_EXPIRATION 900000 (15 min) Access token TTL in ms
JWT_REFRESH_EXPIRATION 604800000 (7 d) Refresh token TTL in ms
CORS_ALLOWED_ORIGINS http://localhost:5173,http://localhost:3000 Comma-separated allow-list
UPLOAD_DIR uploads (container: /app/uploads) File-storage directory
FRONTEND_RESET_URL http://localhost:5173/reset-password Prepended to password-reset tokens
MAIL_HOST / MAIL_PORT / MAIL_USERNAME / MAIL_PASSWORD mailpit defaults SMTP for password-reset emails
MAIL_FROM no-reply@chathub.local Sender address
RATE_LIMIT_CAPACITY / RATE_LIMIT_REFILL_SECONDS 100 / 60 Per-IP token-bucket limit (skips /ws, /uploads, swagger)
SPRING_PROFILES_ACTIVE default prod disables alice / bob seeding

REST API

Base path: /api. Every protected endpoint requires Authorization: Bearer <accessToken>. Refresh tokens are issued alongside the access token and rotated on every call to /auth/refresh.

Auth (/api/auth)

Method Path Description
POST /register Create an account; returns access + refresh tokens.
POST /login Authenticate with username-or-email + password.
POST /refresh Exchange a refresh token for a fresh pair (rotates).
POST /logout Revoke a refresh token.
POST /forgot-password Email a one-time reset link.
POST /reset-password Consume a reset token + set a new password.
GET /me Currently signed-in user.

Users (/api/users)

Method Path Description
GET /search?q=&page=&size= Search the directory by username / display name.
GET /{id} Fetch a user.
PUT /me Update display name / bio.
PUT /me/avatar Multipart upload of a new avatar.

Chats (/api/chats)

Method Path Description
GET / All chats for the current user.
GET /search?q= Search the user's groups.
GET /{roomId} Fetch one chat (member-only).
POST /private Open or fetch a 1:1 chat with a target user.
POST /group Create a group { name, memberIds[] }.
PUT /group/{roomId}/rename Rename a group.
POST /group/{roomId}/members Add member { userId }.
DELETE /group/{roomId}/members/{userId} Remove a member (or leave).

Messages (/api/messages)

Method Path Description
POST / Send a message into a room.
GET /room/{roomId}?page=&size= Paged history (newest first).
GET /room/{roomId}/pinned Pinned messages.
GET /search?q=&page=&size= Full-text search across the user's rooms.
PUT /{messageId} Edit content (author only).
DELETE /{messageId} Soft-delete (author only).
PUT /{messageId}/pin?pinned= Pin / unpin (room members).
PUT /room/{roomId}/read?lastReadMessageId= Update read-pointer.

Notifications (/api/notifications)

Method Path Description
GET / Recent in-app notifications.
GET /unread-count { count }.
PUT /{id}/read Mark one read.
PUT /read-all Mark all read.

Files (/api/files)

Method Path Description
POST /upload Multipart file; returns { url, name, contentType, size }.

Uploaded files are served back via /uploads/<id>.<ext>. Validated by content-type and extension; max 15 MB.

Admin (/api/admin, role ADMIN)

Method Path Description
GET /stats Aggregate counters.
GET /users?page=&size= Paginated user list.
PUT /users/{userId}/status?enabled= Suspend / re-activate.
PUT /users/{userId}/role?role= Promote / demote.

Admins cannot suspend or change the role of their own account.


Realtime API (STOMP over SockJS)

WebSocket handshake endpoint: /ws (SockJS). The JWT access token is sent as the STOMP Authorization: Bearer <token> header on CONNECT.

Server → client subscriptions

Destination Payload Notes
/topic/chat.{roomId} ChatSocketEvent (envelope { type, chatRoomId, payload }) Room events: NEW_MESSAGE, EDIT_MESSAGE, DELETE_MESSAGE, PIN_MESSAGE, TYPING, READ_RECEIPT, GROUP_RENAMED, MEMBER_ADDED, MEMBER_REMOVED.
/topic/presence { userId, online, lastSeen } Global presence broadcast.
/user/queue/chats ChatRoomDto or ChatSocketEvent (ROOM_UPDATED, MEMBER_REMOVED) Sidebar updates: new room, last-message bump, removed-from-group.
/user/queue/notifications NotificationDto In-app notifications targeted at the current user.

Client → server publishes

Destination Payload Effect
/app/chat.typing { chatRoomId, typing: boolean } Broadcasts a typing event to room members.
/app/chat.read { chatRoomId, lastReadMessageId } Updates the sender's read-pointer + broadcasts READ_RECEIPT.

Sending a message is a plain REST POST /api/messages; the resulting NEW_MESSAGE event is fanned out to the room topic and triggers a ROOM_UPDATED event for every other member's sidebar queue.


Architecture notes

  • Stateless API + stateful WebSocket. Spring Security uses STATELESS session creation; the JWT filter authenticates each REST request, and a ChannelInterceptor authenticates each STOMP CONNECT frame with the same access token.
  • Refresh-token rotation. Refresh tokens are opaque random strings persisted in refresh_tokens with expires_at and revoked flags. On every /auth/refresh call the presented token is revoked and a fresh one is issued; suspending an account revokes all of that user's refresh tokens.
  • Presence. A WebSocketEventListener reacts to SessionConnected / SessionDisconnect, updates users.online + users.last_seen, and broadcasts a PresenceEvent to /topic/presence. The frontend merges the event stream with the seed state from each room's member list.
  • Read pointers. chat_members.last_read_message_id is the single source of truth for unread counts (countByChatRoomIdAndIdGreaterThanAndSenderIdNot). The frontend marks-read both on REST history fetch and on every NEW_MESSAGE arriving in the active room.
  • Soft-delete. Messages set deleted=true and the DTO mapper nulls their content; the UI renders a "This message was deleted" pill.
  • Rate limiting. A simple per-IP fixed-window filter (RateLimitFilter, capacity / refill configurable) runs ahead of Spring Security and emits the standard ApiError JSON envelope.
  • Auditing. Every entity extends BaseEntity with @CreatedDate / @LastModifiedDate timestamps.
  • Scheduled cleanup. ScheduledTasks deletes expired refresh + password-reset tokens hourly.

Security

  • BCrypt strength 12 for password hashing.
  • HS256-signed JWTs carrying userId, username, role claims.
  • Refresh tokens are opaque DB-stored values (not JWTs) — instantly revocable.
  • All admin endpoints are double-locked (URL match in SecurityConfig and @PreAuthorize("hasRole('ADMIN')")).
  • File uploads are validated by allow-listed content-type and extension and capped at 15 MB.
  • CORS allow-list is explicit (no *); credentials are not exposed in the access token URL.
  • Authentication principal is bound onto the STOMP session so @MessageMapping handlers know exactly who sent the frame.

For production: set a fresh JWT_SECRET (≥256 bits, base64-encoded), point MAIL_* at a real SMTP provider, set SPRING_PROFILES_ACTIVE=prod to skip demo seeding, and front the stack with HTTPS (the nginx config in frontend/ is the natural place to terminate TLS).


Scripts & commands

# Backend
cd backend
mvn spring-boot:run           # dev run (requires Postgres)
mvn clean package             # build the fat jar (target/realtime-chat-hub-1.0.0.jar)
mvn test                      # run tests

# Frontend
cd frontend
npm run dev                   # Vite dev server on :5173
npm run build                 # production bundle into dist/
npm run preview               # preview the production bundle
npm run lint                  # ESLint

Build notes & honest limitations

  • The backend was developed and statically audited in an environment that did not have access to Maven Central (sandbox network policy blocked repo.maven.apache.org). Maven 3.9.x and JDK 21 are installed; the code compiles cleanly given a working Maven dependency cache, and the supplied backend/Dockerfile is the recommended way to build it (it downloads dependencies as part of the mvn package step). On any normal developer machine or CI runner with internet access, mvn -DskipTests package inside backend/ will produce the runnable jar.
  • The frontend was built end to end in this environment (Vite production build, 252 modules, no errors / warnings) and the resulting bundle is what frontend/Dockerfile reproduces.
  • All wiring between the frontend and the backend was cross-checked by reading the DTO records, the controller signatures, and the STOMP destinations — they match exactly. Where the API contract has rough edges (e.g. NotificationDto.referenceId carrying the room id for NEW_MESSAGE), the conventions are documented above.
  • application.yml uses spring.jpa.hibernate.ddl-auto=update, which is fine for development. For production migrations you'll likely want to introduce Flyway or Liquibase.

License

MIT License

Copyright (c) 2026

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

Full-stack real-time chat — STOMP/WebSocket messaging, JWT auth with refresh-token rotation, group chats, file sharing, online presence, Docker Compose

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages