A minimal chat example built with FastAPI + WebSocket + Postgres. The frontend uses a Next.js + React chat panel UI.
backend/main.py: FastAPI app bootstrap (CORS enabled)backend/routes.py: Aggregates HTTP + WebSocket routersbackend/http_routes.py: Auth + chat HTTP routesbackend/ws_routes.py: Chat WebSocket routesbackend/ws_state.py: Shared WebSocket connection state/helpersbackend/auth.py: JWT helpers and auth endpointsbackend/emailer.py: SMTP email sender for verification codesbackend/db.py: Postgres storage for users, conversations, messagesbackend/db_postgres.py: Postgres facade (exports storage helpers)backend/db_postgres_core.py: Postgres pool + configbackend/db_postgres_schema.py: Postgres schema/bootstrapbackend/db_postgres_users.py: User + verification storagebackend/db_postgres_messages.py: Message storagebackend/db_postgres_conversations.py: Conversation + membership storagefrontend/app/login/page.tsx: Login/sign up UIfrontend/app/chat/page.tsx: Chat workspace UIfrontend/app/page.tsx: Root redirect to login/chatfrontend/app/globals.css: styling
Must be Python3.10
On windows:
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txtOn macOS/Linux:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txtuvicorn backend.main:app --host 0.0.0.0 --port 8000 --reloadDefault base URL: http://localhost:8000
- Backend reads from root
.env(loaded bybackend/main.py). - Frontend reads from
frontend/.env.local. .env_egis a template only and is not loaded automatically.
Rate limiting is enabled by default (in-memory, per-process). Toggle with:
CHAT_RATE_LIMIT_ENABLED=1
HTTP endpoints (all require Authorization: Bearer <token>):
POST /auth/signup
POST /auth/login
GET /auth/me
GET /bootstrap
POST /auth/verify
POST /auth/resend-verification
PATCH /users/me
GET /messages/{conversation_id}?before_id=123&limit=30
GET /messages/{conversation_id}?since_id=123
GET /conversations
PATCH /conversations/{conversation_id}
GET /conversations/{conversation_id}/members
POST /conversations/{conversation_id}/members
DELETE /conversations/{conversation_id}/members/{member_id}
POST /conversations/{conversation_id}/read
POST /conversations
GET /invites
POST /invites/{invite_id}/accept
POST /invites/{invite_id}/decline
WebSocket endpoint (token required as query param or first auth message):
ws://localhost:8000/ws/chat/{user_id}
After connect, send:
{"type":"auth","token":"YOUR_JWT"}Legacy query param still works:
ws://localhost:8000/ws/chat/{user_id}?token=YOUR_JWT
cd frontend
npm install
npm run devOpen http://localhost:3000 in your browser.
Routes:
/loginfor sign in / sign up/chatfor the chat workspace (requires verified email)
Optional: override the API base URL (defaults to http://localhost:8000):
NEXT_PUBLIC_API_BASE=http://localhost:8000 npm run devYou can also point to a different host (LAN/VPN); the frontend will honor the explicit env value even if it differs from the page host.
If WebSocket access is blocked on port 8000, the frontend dev server proxies
/ws to the backend. Set the backend target before starting:
NEXT_PUBLIC_API_BASE=http://172.25.223.62:8000 BACKEND_WS=ws://172.25.223.62:8000 npm run devIf you need to allow non-localhost dev origins, set a comma-separated list:
NEXT_DEV_ORIGINS=http://192.168.1.10:3000,http://my-host:3000 npm run devBackend (pytest):
pip install -r requirements.txt
pip install pytest
pytest backend/testsFrontend (Playwright):
cd frontend
npm install
npx playwright install
npm run test:e2eNote: the Playwright test expects the frontend to be running at
PLAYWRIGHT_BASE_URL (defaults to http://localhost:3000).
Local dev uses Postgres via DATABASE_URL:
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/postgres
Use a schema name if you want to isolate test data:
CHAT_DB_SCHEMA=test
Reset Postgres (drops all chat tables):
python scripts/reset_postgres.py
Run a local Postgres just for pytest:
docker compose -f docker-compose.test.yml up -d
Then run tests:
pytest backend/tests
Stop the test database:
docker compose -f docker-compose.test.yml down
Set the following environment variables for SMTP:
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM="Chat Support <your-email@gmail.com>"
SMTP_USE_TLS=true
Verification rules:
- 6-digit code, expires in 10 minutes
- Resend allowed every 60 seconds
- Unverified users can log in but cannot access chat or WebSocket
- Invite endpoints require verified accounts
- Sign up or log in to get a token (the frontend stores it automatically).
- Click
Connectto open the WebSocket. - Click a conversation to load history via
GET /messages/{conversation_id}. - Type a message and click
Send. - Other connected users receive messages in real-time via WebSocket.
PATCH /users/meupdates the display name (must be non-empty).- Other participants see the updated display name; your own messages still show
You.
- This is a minimal example without auth or a message queue.
- Conversations support optional names via
PATCH /conversations/{conversation_id}. - The chat UI supports local display names, draft persistence, failed-send retry, typing/active indicators, and local message search with highlights (in Settings).
- Self-message recall is supported via right-click (desktop) or long-press (mobile); recall cancels the local message only.
- Quotes use a structured bubble (similar to WeChat/Instagram) and do not pollute message text.
- WebSocket connections are limited to 3 per user; excess connections are rejected with a reason.
- New members join via chat invites; acceptance is required before membership is added.
- UI errors show a short message plus an error code; see
docs/error-codes.mdfor mappings. - Disabled endpoints return
E_ENDPOINT_DISABLED(see error codes doc). - Backend logs HTTP and unhandled errors to stdout; set
LOG_LEVELto control verbosity. - WebSocket rejects non-membership actions (close code 4003) without logging users out.