Notes: Task is done completely and tested Used model Opus 3.7 and Sonnet 3.6 Used Agentic framework It took more than 12 hours of work, several times Cluade make pause with token limit 0 even it is in max subscription What helped : Planning, proper requirements, coverage with unit integration ui playwright and perf k6 tests What slows: constant asking for permission to execute, claude pauses limits of token
A web-based chat service for up to 300 concurrent users — account auth, presence, public/private rooms with roles, friends / DMs / bans, image & file attachments, and notifications. Hackathon submission built to the spec in requirements_to_project.
| Layer | Choice |
|---|---|
| Client | React 18 · Vite · TypeScript · TanStack Query · React Hook Form · Tailwind CSS · shadcn primitives |
| Server | Node 20 · TypeScript (strict) · Express · ws · Zod · Pino · Multer |
| Database | PostgreSQL 16 via Drizzle ORM (drizzle-kit migrations) |
| Cache / PS | Redis 7 via ioredis (presence, pub/sub fan-out, rate-limit buckets) |
| Testing | Vitest (unit + integration) · Playwright (E2E) · k6 (load) |
| Runtime | Docker Compose (db, redis, server, client) |
docker compose upThat's the only command needed from a fresh clone. It builds and starts every service in the right order and seeds the database on first boot.
Open these URLs to verify:
| What | URL |
|---|---|
| Client (React SPA) | http://localhost:5173 |
| Server health probe | http://localhost:3001/healthz |
| WebSocket endpoint | ws://localhost:3001/ws (auth needed) |
| PostgreSQL | postgres://app:app@localhost:5432/app |
To wipe volumes and rebuild from scratch: npm run dev:reset.
- Register — open the client, click "Register", fill the form. You're signed in immediately and land on the dashboard.
- Create a room — go to Browse rooms, click Create room, pick
public. You're auto-joined as owner. - Invite / DM a friend — on the Contacts page, add a friend by username, accept the request in the other account, then click Message to open a DM. For private rooms, use Manage room → Invitations and send an invite; the recipient sees it in their Invitations inbox.
- Send a message / upload an image — in the chat pane, type a message and press
Enter. Drag an image into the composer, paste one from the clipboard, or click Attach. Peers receive the frame in < 50 ms on localhost; an unread badge bumps in every other room the recipient isn't looking at.
Keyboard shortcuts inside a chat route: Enter sends, Shift+Enter newlines, / focuses the composer from anywhere, Cmd/Ctrl+K jumps to the room catalog, Esc closes dialogs.
Every bullet maps to a requirements_to_project section.
- §2.1 Auth & sessions — email + username + password registration, signed session cookies, active-sessions list, per-device sign-out, change password, forgot/reset, account deletion with cascade.
- §2.2 Presence —
online / afk / offlineper user, AFK afterAFK_THRESHOLD_MS(default 60 s), fan-out scope = self ∪ co-room ∪ friends minus bans. - §2.3 Friends & DMs & user bans — friend requests (send / accept / decline / cancel), friend list, user-to-user bans, frozen-DM banner when one side blocks the other.
- §2.4 Rooms & moderation — public catalog, private rooms by invitation, owner / admin / member roles, promote / demote, remove (= ban), unban, owner-only delete, room settings (name / description / visibility).
- §2.5 Messaging — sub-second WS delivery, reply, edit, delete (tombstones), 3 KB body limit, infinite-scroll history.
- §2.6 Attachments — up to 4 images or files per message, 10 MB each, served from the server's upload directory, orphan GC at boot.
- §2.7 Notifications — per-conversation unread counters pushed on
unread.update, invitation received / responded, room role changes, member joined / left, presence transitions.
Classic three-tier split. The client is a Vite-bundled React SPA that talks to the server over REST for request/response work and over one authenticated WebSocket for push traffic. The server persists to PostgreSQL (Drizzle ORM + drizzle-kit migrations) and uses Redis for pub/sub fan-out across the WS process + per-tab presence keys + rate-limit buckets. Files are stored on the server's local disk under UPLOAD_DIR, streamed back through /api/attachments/:id. All boundary contracts (HTTP bodies, WS frames) live in shared/ as Zod schemas and are shared between both ends — the discriminated union wsMessage enumerates every frame.
npm run typecheck # all three workspaces
npm run lint # eslint, zero warnings
npm test # vitest unit + integration — 234 tests
npm run e2e # Playwright — 24 specs (needs stack running)
npm run load # k6 chat-300-users scenario (needs stack running)The load test's k6 script can also run through the official Docker image — no host install needed:
docker run --rm -i --add-host=host.docker.internal:host-gateway \
--env API_URL=http://host.docker.internal:3001 \
--env WS_URL=ws://host.docker.internal:3001/ws \
grafana/k6 run - <tests/load/chat-300-users.jsSee tests/load/README.md and tests/load/REPORT.md for the latest numbers — p95 chat delivery ≈ 26 ms at 300 concurrent users, well under the 3 s budget.
shared/ Zod contracts (ws-messages, http-contracts, enums, ids)
server/ Express + ws + Drizzle
client/ React + Vite
db/init/ Postgres first-boot SQL (extensions + test DB)
tests/e2e/ Playwright specs
tests/load/ k6 scripts + report
docker compose.yml ships sensible defaults. Copy .env.example to .env to override. Common knobs: DATABASE_URL, REDIS_URL, CLIENT_ORIGIN, UPLOAD_DIR, SESSION_COOKIE_NAME, AFK_THRESHOLD_MS, PRESENCE_TAB_TTL_SECONDS.
§6 Jabber federation is not implemented. The scope is called out in the requirements but was deprioritised in favour of finishing §1–§5 to a submittable polish; the WS protocol + domain model are shaped so a future XEP-xxxx bridge can translate frames without schema changes.
Full test run: all checks pass ✓
| Suite | Result |
|---|---|
npm run typecheck |
✅ green across 3 workspaces |
npm run lint |
✅ green (0 warnings) |
npm test (server) |
✅ 307/307 pass |
npm test (client) |
✅ 32/32 pass |
| Unit + integration total | ✅ 339 pass |
npm run e2e |
✅ 32/32 Playwright pass |
| k6 chat — 300 VUs × 90 s | ✅ 300/300 connected, 0 errors, p95 delivery 45 ms (budget 3000 ms), p99 78 ms, all thresholds pass |
| k6 presence — 50 VUs × 45 s | ✅ transition p95 16 ms (budget 2000 ms), p99 ~45 ms |