Chat with friends in real time and receive AI‑generated smart replies
AI summary · website content
SocialJava · JavaScriptGitHub @manshasharma2323-creatorOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗AI-Powered Distributed Real-Time Chat Application
A full-stack, real-time chat application with JWT authentication, live WebSocket messaging, MySQL persistence, and AI-powered Smart Reply suggestions. Built as a two-project monorepo — a Spring Boot API/WebSocket backend and a static HTML/CSS/JS frontend — fully decoupled and independently deployable.
Backend/ Spring Boot 3 + WebSocket (STOMP) + Spring AI + MySQL (Java/Maven)
Frontend/ Static HTML/CSS/JS client (no build step, deploys as-is)
AI Chat Hub is a two-person-to-two-person (1:1) real-time messaging application: users register, log in, see who else is registered, and chat with them instantly over a WebSocket connection, with full message history persisted in MySQL. An AI layer (OpenAI or a local Ollama model, interchangeably) powers two features on top of that: Smart Reply (context-aware suggested responses inside a conversation) and a standalone AI Assistant chat page.
Most "build a chat app" tutorials stop at either the real-time messaging part or the AI part, rarely both, and rarely with production concerns (auth, CORS, deployment, secret handling) treated seriously. This project sets out to build a chat application that is simultaneously:
localhost values that only work on one
machine.GET /api/chat/history,
GET /api/chat/conversations).| Layer | Technology |
|---|---|
| Backend | Java 21, Spring Boot 3.5, Spring Security, Spring Data JPA, Spring WebSocket (STOMP), Spring AI (OpenAI + Ollama), JJWT, BCrypt, Maven |
| Database | MySQL 8 (Hibernate ddl-auto=update) |
| Frontend | HTML5, CSS3, vanilla JavaScript (ES2017+), SockJS, @stomp/stompjs — no framework, no build step |
| Testing | JUnit 5, Spring Boot Test, AssertJ, H2 (in-memory, test-only) |
| Deployment | Vercel (frontend, static), any JVM host — Render/Railway/Fly.io/EC2 (backend) |
flowchart LR
subgraph Client
FE["Frontend<br/>Static HTML/CSS/JS<br/>(Vercel)"]
end
subgraph Backend["Spring Boot Backend"]
API["REST Controllers"]
WS["WebSocket / STOMP"]
SVC["Service Layer"]
REPO["Repository Layer<br/>(Spring Data JPA)"]
end
DB[("MySQL")]
FE -- "HTTPS REST, JWT bearer" --> API
FE -- "WSS, STOMP over SockJS" --> WS
API --> SVC
WS --> SVC
SVC --> REPO
REPO --> DB
Every request — REST or WebSocket — passes through the same layering:
Controller → Service → Repository → MySQL. Controllers never talk to
the database directly, and DTOs (not JPA entities) are what actually
cross the wire, so internal fields (ids, password hashes, the read
flag) never leak into an API response by accident.
The frontend is a plain static site — no React, no Vite, no build step — by design, so it deploys as-is to any static host. Every authenticated page follows the same pattern:
assets/config.js — sets window.CHATAPP_API_BASE (the one
deployment-specific value): http://localhost:8080 automatically on
localhost, and PROD_API_BASE (edit this) everywhere else.assets/api.js — shared module: session storage (JWT in
localStorage), an authenticated fetch wrapper that auto-redirects
to login on a 401, "Remember me" session handling (localStorage vs
sessionStorage), formatting helpers, toasts, and the shared
sidebar shell.assets/<page>.js — the page's own logic, e.g. messages.js owns the
WebSocket connection and chat UI; login.js owns the login form.Pages: index.html (public landing page), login.html, register.html,
then behind auth: dashboard.html, messages.html, contacts.html,
chat-history.html, ai-assistant.html, profile.html, settings.html,
about.html.
com.mansha.chatapp
├── config/ WebSocket setup, AI client beans, AI provider auto-detection,
│ Ollama warmup, WebSocket connect/disconnect listeners
├── controller/ REST (@RestController) + STOMP (@MessageMapping) endpoints
├── dto/ Request/response shapes — never expose entities directly
├── entity/ JPA entities: User, ChatMessage
├── exception/ @RestControllerAdvice — turns exceptions into consistent JSON
├── repository/ Spring Data JPA repositories
├── security/ JwtService, JwtAuthenticationFilter, JwtChannelInterceptor,
│ TokenBlacklistService, SecurityConfig (CORS + auth rules)
└── service/ Business logic: ChatService, PresenceService,
SmartReplyService, AiAssistantService, UserService
Controllers are intentionally thin — validation and business rules live in
the service layer (e.g. ChatService.saveMessage rejects empty content,
self-messaging, and unknown recipients before anything touches the
database), so the same rules apply whether a message arrives over the REST
API or the WebSocket.
Two tables, MySQL, managed by Hibernate (ddl-auto=update — no manual
migrations needed):
erDiagram
USERS ||--o{ CHAT_MESSAGES : "sends/receives (by email)"
USERS {
bigint id PK
varchar name
varchar email UK
varchar password "bcrypt hash"
}
CHAT_MESSAGES {
bigint id PK
varchar sender_email
varchar receiver_email
text content
datetime sent_at
boolean is_read
}
users.email is the identity used everywhere — in JWTs, WebSocket
principals, and message rows — rather than a numeric foreign key, which
keeps the message table decoupled from user-table internals.chat_messages has composite indexes on (sender_email, receiver_email)
and sent_at to keep conversation history and unread-count queries fast
as message volume grows.password
field is @JsonProperty(WRITE_ONLY), so it's accepted on the way in
(register/login) but can never be serialized back out in any API
response.JwtService) with the user's email
as the subject and a 24-hour expiry.JwtAuthenticationFilter, which validates the Authorization: Bearer <jwt> header before Spring Security's authorization check runs.JwtChannelInterceptor on
the STOMP CONNECT frame — the same validation, so there is no
"authenticated over REST but not over WebSocket" gap.POST /api/users/logout adds the
token to an in-memory TokenBlacklistService, checked by
JwtService.extractEmail — so a signed-out token stops working
immediately, instead of silently remaining valid for the rest of its
24-hour lifetime.BCryptPasswordEncoder). Legacy
plaintext accounts (if any existed before hashing was added) are
transparently upgraded to a hash the moment they next log in
successfully./ws (SockJS-wrapped STOMP, degrades gracefully without
native WebSocket support; also reachable as a raw WebSocket at
/ws/websocket)./app/chat.send with
{ receiverEmail, content }. The server validates and persists it, then
delivers it to both the sender's and receiver's private queue
(/user/queue/messages) — so every open tab of either user stays in
sync, and the sender sees their own message appear immediately too./app/chat.typing → relayed live to
/user/queue/typing, never persisted./topic/presence.
Tracked per WebSocket session (not just per user), so multiple open
tabs/devices don't flip a user to "offline" until the last session
disconnects.chat.send (empty message, unknown receiver,
messaging yourself) is reported back to the sender on
/user/queue/errors instead of vanishing silently into a generic STOMP
error frame.flowchart LR
A["New chat message<br/>arrives"] --> B["Frontend requests<br/>Smart Reply"]
B --> C["POST /api/ai/smart-replies<br/>{ peerEmail }"]
C --> D["SmartReplyService pulls last<br/>10 messages of the conversation"]
D --> E{"AI provider<br/>(OpenAI or Ollama)"}
E -- "responds in time" --> F["Parse into 3 short,<br/>tonally-distinct suggestions"]
E -- "fails / times out" --> G["Static fallback<br/>suggestions"]
F --> H["Frontend renders<br/>suggestion chips"]
G --> H
H --> I["User clicks a chip →<br/>fills the message input"]
I --> J["User edits and sends<br/>like any normal message"]
AiProviderResolver): app.ai.provider
defaults to auto — picks openai automatically when a real
OPENAI_API_KEY is configured, otherwise ollama. Can be forced either
way via APP_AI_PROVIDER.AI_SMART_REPLY_TIMEOUT_SECONDS (default 12s) for the AI; the AI
Assistant waits AI_ASSISTANT_TIMEOUT_SECONDS (default 35s). Past that,
Smart Reply falls back to generic static suggestions rather than
hanging the chat UI — verified live: a genuine AI timeout still returns
a normal 200 response with usable suggestions./api/ai/smart-replies and /api/ai/assistant with a JWT, never an AI
provider directly.chatapp/
├── Backend/
│ ├── src/main/java/com/mansha/chatapp/
│ │ ├── config/ WebSocket, AI client, AI provider resolver, warmup, WS lifecycle
│ │ ├── controller/ REST + STOMP @MessageMapping controllers
│ │ ├── dto/ Request/response shapes
│ │ ├── entity/ JPA entities (User, ChatMessage)
│ │ ├── exception/ Centralized error → JSON mapping
│ │ ├── repository/ Spring Data JPA repositories
│ │ ├── security/ JWT service/filter/channel-interceptor, token blacklist, SecurityConfig
│ │ └── service/ Business logic (chat, presence, AI, users)
│ ├── src/main/resources/application.properties main config (env-var driven)
│ ├── src/test/ JUnit tests, running against an isolated H2 DB
│ ├── .env.example documents every backend env var (placeholders only)
│ └── pom.xml
├── Frontend/
│ ├── assets/ one .js/.css pair per page, plus shared api.js/theme.css/config.js
│ ├── index.html public landing page
│ ├── login.html, register.html
│ ├── dashboard.html, messages.html, contacts.html, chat-history.html
│ ├── ai-assistant.html, profile.html, settings.html, about.html
│ └── vercel.json static-site deploy config (see Production deployment below)
├── .gitignore
└── README.md
./mvnw / mvnw.cmd wrapper — no separate install needed)DB_URL)llama3.2)git clone https://github.com/manshasharma2323-creator/chatapp.git
cd chatapp
Then follow MySQL setup and Running the application below.
CREATE DATABASE ai_chat_app;
Tables are created/updated automatically on startup (spring.jpa.hibernate.ddl-auto=update) —
no manual migration step needed. Default local credentials are root/root
(override with DB_USERNAME/DB_PASSWORD if yours differ).
All of these have safe local-dev defaults baked into
Backend/src/main/resources/application.properties — you only need to set
them for production, or to override a default. See also
Backend/.env.example for a copyable reference (placeholders only, no
real values).
| Variable | Purpose | Default |
|---|---|---|
PORT | Port the backend listens on — most PaaS hosts (Render, Railway, Heroku) inject this at runtime | 8080 |
DB_URL | JDBC URL for MySQL | jdbc:mysql://localhost:3306/ai_chat_app |
DB_USERNAME | MySQL username | root |
DB_PASSWORD | MySQL password | root |
JWT_SECRET | HMAC signing key for JWTs — must be set to a real random secret before deploying | unset → a random per-process secret (logins reset on every restart; a warning is logged). Must be ≥ 32 chars when set |
ALLOWED_ORIGINS | Comma-separated list of origins allowed to call the API / open a WebSocket (CORS) | localhost dev ports + the deployed Vercel frontend |
JPA_SHOW_SQL | Log every SQL statement (debugging only) | false |
OPENAI_API_KEY | OpenAI API key, only needed to use the openai provider | unset |
APP_AI_PROVIDER | Force openai or ollama; leave unset to auto-pick openai when a real OPENAI_API_KEY is set, otherwise ollama | auto |
OLLAMA_BASE_URL | Where to reach Ollama, if it's not on the same machine as the backend | http://localhost:11434 |
AI_SMART_REPLY_TIMEOUT_SECONDS | How long Smart Reply waits for the AI before falling back to static suggestions | 12 |
AI_ASSISTANT_TIMEOUT_SECONDS | How long the AI Assistant waits before returning an error | 35 |
The frontend has exactly one configuration point:
Frontend/assets/config.js — the backend's base URL. On
localhost/127.0.0.1 it targets http://localhost:8080 automatically;
on any other host it uses PROD_API_BASE, which you must set to your
backend's public https URL. While it is empty, a deployed site shows a
"not connected to a backend" banner and login/register refuse to run (it
never falls back to the visitor's own localhost:8080).
Never commit real values for any of these. Backend/.env.example
exists precisely so real secrets never need to touch the repository —
copy it, fill in real values locally or in your host's dashboard, and
keep the copy out of git (see Security considerations).
Backend:
cd Backend
./mvnw spring-boot:run
Runs on http://localhost:8080 (or $PORT if set).
Frontend:
cd Frontend
npx serve .
Any static file server works — there's no build step. By default
Frontend/assets/config.js points at http://localhost:8080, matching
the backend above.
Then open the served frontend URL in a browser, register an account, and sign in.
All /api/** routes except register/login require
Authorization: Bearer <jwt>. Only endpoints that actually exist in the
code are documented here.
Request:
{ "name": "Ada Lovelace", "email": "[email protected]", "password": "password123" }
Response 200 OK:
{ "id": 1, "name": "Ada Lovelace", "email": "[email protected]" }
(password is never included in the response — write-only field)
Request:
{ "email": "[email protected]", "password": "password123" }
Response 200 OK (raw JWT string body):
"eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJhZGFAZXhhbXBsZS5jb20i..."
401 Unauthorized on wrong password or unknown email.
Header: Authorization: Bearer <jwt>. Response 200 OK, empty body. The
token is rejected on every subsequent request from that point on.
Response 200 OK:
{ "name": "Ada Lovelace", "email": "[email protected]" }
Request (all fields optional — send only what changed):
{ "name": "Ada L.", "currentPassword": "password123", "newPassword": "newpassword456" }
Response 200 OK: same shape as GET /api/users/me. 400 Bad Request if
currentPassword doesn't match or newPassword is under 6 characters.
Response 200 OK:
[ { "name": "Bob", "email": "[email protected]" } ]
Response 200 OK:
[
{
"peerEmail": "[email protected]",
"lastMessage": "See you at 10am!",
"lastMessageAt": "2026-09-18T21:41:47.90",
"lastMessageMine": false,
"unreadCount": 1
}
]
Response 200 OK:
[
{ "senderEmail": "[email protected]", "receiverEmail": "[email protected]", "content": "Hi!", "sentAt": "2026-09-18T21:40:01.12" }
]
Response 200 OK:
[ { "email": "[email protected]", "online": true } ]
Response 200 OK, empty body.
Response 200 OK:
{ "[email protected]": 2 }
Request:
{ "peerEmail": "[email protected]" }
Response 200 OK (always — falls back rather than erroring):
{ "suggestions": ["Sounds good", "Tell me more", "Got it, thanks"] }
Request:
{ "message": "Explain WebSockets simply", "history": [{ "role": "user", "content": "Hi" }, { "role": "assistant", "content": "Hello! How can I help?" }] }
Response 200 OK:
{ "reply": "A WebSocket is a persistent two-way connection..." }
400 Bad Request if message is blank. 503 Service Unavailable if the
AI provider fails or times out.
Endpoint: /ws (STOMP over SockJS). Connect with
Authorization: Bearer <jwt> as a STOMP CONNECT header.
| Direction | Destination | Payload |
|---|---|---|
| publish | /app/chat.send | { receiverEmail, content } |
| publish | /app/chat.typing | { receiverEmail, typing } |
| subscribe | /user/queue/messages | Incoming/echoed chat messages |
| subscribe | /user/queue/typing | Peer typing status |
| subscribe | /user/queue/errors | Rejected send errors |
| subscribe | /topic/presence | { email, online } broadcasts |
See AI / Smart Reply architecture above
for the full diagram. In short: POST /api/ai/smart-replies with
{ peerEmail } → backend pulls the last 10 messages of that conversation
→ asks the configured AI provider for exactly 3 short, distinct reply
suggestions → returns them, or falls back to generic suggestions within
AI_SMART_REPLY_TIMEOUT_SECONDS if the provider is slow/unreachable.
Backend — deploy the Spring Boot app anywhere that runs a JAR (Render, Railway, Fly.io, EC2, etc.):
cd Backend
./mvnw clean package
java -jar target/chatapp-0.0.1-SNAPSHOT.jar
Set DB_URL, DB_USERNAME, DB_PASSWORD, JWT_SECRET, and
ALLOWED_ORIGINS (your frontend's exact deployed origin) as environment
variables on whatever platform you use — never in a committed file.
Frontend — deploy Frontend/ on Vercel (or any static host):
Frontend/assets/config.js and set PROD_API_BASE to your
deployed backend's public https URL (no trailing slash). The
backend must also list this site's origin in ALLOWED_ORIGINS.vercel.json at the repo root already tells Vercel this is a
static site (no build command, Frontend/ as the output directory) —
no framework/build configuration needed on Vercel's side.vite: command not found on Vercel — this project has no Vite/React
build; vercel.json disables the build step entirely. If you see this
error, check the Vercel dashboard's Framework Preset is set to Other
and the Build/Install Command overrides are off.PROD_API_BASE in Frontend/assets/config.js and redeploy.JWT_SECRET is
not set, so a random secret is generated per process. Set it.users.email failed — your
existing table contains duplicate emails (accounts that differ only by
case/whitespace). Merge or delete the duplicates, then restart.Frontend/assets/config.js points at the right backend URL, and
that the backend's ALLOWED_ORIGINS includes your frontend's exact
origin (scheme + host, no trailing slash).OPENAI_API_KEY). This is a deliberate fallback, not a bug —
check the backend logs for the underlying AI error (logged as a WARN
from SmartReplyService/AiAssistantService), and consider raising
AI_SMART_REPLY_TIMEOUT_SECONDS on slower/CPU-only Ollama hardware.ALLOWED_ORIGINS),
not a wildcard — required because credentialed cross-origin requests
(which SockJS's transport probing uses) cannot be safely combined with
Access-Control-Allow-Origin: *.JWT_SECRET, DB_PASSWORD, and
OPENAI_API_KEY are all environment-variable-only; the committed
defaults are either obviously-fake placeholders (JWT secret) or generic
local-dev values (root/root for MySQL). Verified via a full scan of
both the current working tree and the entire git history — see
[Remaining issues / repo status] in project reports for details./api/ai/* endpoints with a JWT.Deliberately out of scope for the current version, listed here rather than left unexplained:
SimpleBroker, if the backend ever needs to scale
to multiple instancescd Backend
./mvnw test
Frontend unit tests (Node 18+, no dependencies):
npm test # from the repo root
Backend tests run against an isolated in-memory H2 database
(src/test/resources/application.properties) — they never touch your real
MySQL data. Coverage includes: the full register → login → authenticated
request → logout (with real token-revocation verification) flow, JWT
rejection (missing/invalid token), a genuine two-user WebSocket integration
test (real STOMP connections proving live delivery + persistence + the
error-reporting path), chat message validation, unread-count tracking, and
AI-provider auto-detection for both the "key configured" and "no key
configured" cases.
ApiRegressionTests pins the audit fixes: case-insensitive email
registration/login, 401 (not 403/500) for anonymous or malformed requests,
400/404 (not 500) for bad JSON or unknown routes, UTC Z timestamps,
message/AI-prompt size limits, and the JWT-secret safeguards.