> Paste the contents of any single section (or the whole document) as your opening prompt when starting a new AI coding session. The more context you give upfront, the fewer clarifying rounds you need. This document was reverse-engineered from the real build history of jama across ~9 sessions.
---
## Part 1 — What to Build (Product Brief)
Build a **self-hosted team chat Progressive Web App** called **jama**.
It is a full-stack, single-container application that runs entirely inside Docker. Users install it on a private server and access it via a browser or as an installed PWA on desktop/mobile. There is no cloud dependency — everything (database, uploads, API, frontend) lives in one Docker image.
### Core philosophy
- Simple to self-host (one `docker compose up`)
- No external services required (no Firebase, no Pusher, no S3)
- Works as an installed PWA on Android, iOS, and desktop Chrome/Edge
- Instant real-time messaging via WebSockets (Socket.io)
- Push notifications via Web Push (VAPID), works when app is backgrounded
---
## Part 2 — Tech Stack
| Layer | Technology |
|---|---|
| Backend runtime | Node.js 20 (Alpine) |
| Backend framework | Express.js |
| Real-time | Socket.io (server + client) |
| Database | SQLite via `better-sqlite3` |
| Auth | JWT in HTTP-only cookies + `jsonwebtoken` |
-- Allows each user to set a personal display name for any group/DM
```
### Migration pattern
All schema changes must be additive `ALTER TABLE` statements wrapped in try/catch (column-already-exists errors are ignored). Never drop or recreate tables in migrations — this is a live production DB.
- Re-registers push on `visibilitychange → visible`
- Shows HelpModal if user hasn't dismissed it
### Sidebar.jsx
- Renders public groups (fixed order: default first, then alphabetical)
- Renders private groups (sorted newest-message first)
- DM entries show peer's avatar (if set), `Display Name (real name)` format when peer has a display name
- Unread badge counts (blue dot for unread, numbered badge for @mentions)
- Page title format: `(N) App Name` where N is total unread count
### ChatWindow.jsx
- Renders message list with infinite scroll upward (load older messages on scroll to top)
- Typing indicator with bouncing dots animation
- Header: for DMs shows peer avatar + `Display Name (real name)` title
- Marks messages as read when group becomes active
### Message.jsx features
- Date separators between days
- Consecutive messages from same user collapse avatar (show once)
- Emoji-only messages render larger
- Quick reactions bar (👍❤️😂😮😢🙏) + full emoji picker
- Reply-to preview with quoted content
- Link preview cards (og: meta, fetched server-side to avoid CORS)
- Image messages with lightbox on click
-@mention highlighting
- Delete button (own messages + admin/owner can delete any)
- Long-press on mobile = show action menu
### MessageInput.jsx
- Auto-expanding textarea
-`@` triggers mention picker (searches group members)
- Image attach button (uploads immediately, inserts URL into content)
- Emoji picker button
-`Enter` to send, `Shift+Enter` for newline
- Typing events: emit `typing:start` on input, `typing:stop` after 2s idle
---
## Part 10 — CSS Design System
### Variables (`:root` — day mode)
```css
--primary:#1a73e8;
--primary-dark:#1557b0;
--primary-light:#e8f0fe;
--surface:#ffffff;
--surface-variant:#f8f9fa;
--background:#f1f3f4;
--border:#e0e0e0;
--text-primary:#202124;
--text-secondary:#5f6368;
--text-tertiary:#9aa0a6;
--error:#d93025;
--success:#188038;
--bubble-out:#1a73e8;/* own message bubble */
--bubble-in:#f1f3f4;/* other's message bubble */
--radius:8px;
--radius-lg:16px;
--radius-xl:24px;
--shadow-sm:01px3pxrgba(0,0,0,0.12);
--shadow-md:02px8pxrgba(0,0,0,0.15);
--font:'Google Sans','Roboto',sans-serif;
```
### Dark mode (`[data-theme="dark"]`)
```css
--primary:#4d8fd4;
--primary-light:#1a2d4a;
--surface:#1e1e2e;
--surface-variant:#252535;
--background:#13131f;
--border:#2e2e45;
--text-primary:#e2e2f0;
--text-secondary:#9898b8;
--text-tertiary:#606080;/* NOTE: exactly 6 hex digits — a common typo is 7 */
--bubble-out:#4d8fd4;
--bubble-in:#252535;
```
Dark mode is toggled by setting `document.documentElement.setAttribute('data-theme', 'dark')` and saved to `localStorage`.
### Layout
- Desktop: sidebar (320px fixed) + chat area (flex-1) side by side
- Mobile (≤768px): sidebar and chat stack — only one visible at a time, back button navigates
- Chat area: header + scrollable message list (flex-1, overflow-y: auto) + input fixed at bottom
---
## Part 11 — Docker & Deployment
### Dockerfile (multi-stage)
```dockerfile
# Stage 1: Build frontend
FROMnode:20-alpineASbuilder
WORKDIR/app
COPY frontend/package*.json ./frontend/
RUNcd frontend && npm install
COPY frontend/ ./frontend/
RUNcd frontend && npm run build
# Stage 2: Runtime
FROMnode:20-alpine
RUN apk add --no-cache sqlite python3 make g++
WORKDIR/app
COPY backend/package*.json ./
RUN npm install --omit=dev
RUN apk del python3 make g++
COPY backend/ ./
COPY --from=builder /app/frontend/dist ./public
RUN mkdir -p /app/data /app/uploads/avatars /app/uploads/logos /app/uploads/images
EXPOSE3000
CMD["node","src/index.js"]
```
**Critical**: `help.md` lives at `backend/src/data/help.md` → copied to `/app/src/data/help.md` in the image. It must NOT be placed in `/app/data/` which is volume-mounted and will hide baked-in files.
### docker-compose.yaml
```yaml
version:'3.8'
services:
jama:
image:jama:${JAMA_VERSION:-latest}
restart:unless-stopped
ports:
- "${PORT:-3000}:3000"
environment:
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@jama.local}
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
- ADMPW_RESET=${ADMPW_RESET:-false} # set true to reset admin pw on next start
- JWT_SECRET=${JWT_SECRET:-changeme}
- APP_NAME=${APP_NAME:-jama}
- DEFCHAT_NAME=${DEFCHAT_NAME:-General Chat}
volumes:
- jama_db:/app/data # SQLite + uploads persist here
- jama_uploads:/app/uploads
volumes:
jama_db:
jama_uploads:
```
### Volume gotcha
Named Docker volumes shadow any files baked into that directory at image build time. Any file that must survive across rebuilds AND be editable at runtime goes in the volume. Any file that is bundled with the app and should not be overridden by the volume (like `help.md`) must be stored outside the volume mount path.
---
## Part 12 — PWA / Service Worker
-`manifest.json` is **dynamically served by Express** (not static) so app name and icons update without a rebuild
- Service worker (`sw.js`) handles:
- Asset caching for offline support
- Web Push notifications
- Per-conversation notification grouping by `tag: 'jama-group-{groupId}'`
- Skip notification if app window is currently visible (`clients.matchAll`)
- App badge sync via `SET_BADGE` message from the app
- Clear badge on notification click
---
## Part 13 — Features Checklist
### Messaging
- [x] Text messages with URL auto-linking
- [x]@mention with inline picker (type `@` to trigger)
- [x] Image upload + display with lightbox
- [x] Link preview cards (og: meta, server-side fetch)
- [x]`overscroll-behavior-y: none` (prevents pull-to-refresh viewport shift in PWA)
---
## Part 14 — Known Gotchas & Decisions
These are things that will catch you out if you don't know them upfront.
| Gotcha | Solution |
|---|---|
| `auth.js` exports `{ authMiddleware }` as an object | Always destructure: `const { authMiddleware } = require('../middleware/auth')` |
| Docker named volume shadows baked-in files | Never put runtime-required bundled files in `/app/data/` |
| Dark mode `--text-tertiary` hex typo | Value must be exactly 6 hex digits: `#606080` not `#6060808` |
| `last_message_user_id` not set on live updates | Must include `last_message_user_id: msg.user_id` in the `setGroups` update — not just content and timestamp |
| Mobile push subscriptions drop when PWA is backgrounded | Re-register push on every `visibilitychange → visible` event |
| Socket.io reconnect on mobile PWA resume | Add `visibilitychange` handler that calls `socket.connect()` if disconnected |
| Minimized window doesn't count unread in active chat | Check `document.visibilityState === 'hidden'` before skipping unread increment |
| JSX edits with `sed` break on special characters | Use Python string replacement for all JSX file edits |
| Multiple named exports vs default exports | Routes that need to share `io` accept it as a parameter: `module.exports = (io) => router` |
| Page title overwritten on settings refresh | Preserve `(N)` prefix: `const prefix = document.title.match(/^(\(\d+\)\s*)/)?.[1] \|\| ''` |
| `navigator.setAppBadge` not in service worker scope | Use `self.navigator.setAppBadge` inside `sw.js` |
---
## Part 15 — Prompt Engineering Notes
When using this prompt with an AI coding assistant, these practices produced the best results across the 9 build sessions:
### Do
- **Give the full schema upfront** — the AI can design all routes consistently without asking
- **Specify the exact npm packages** — prevents the AI choosing different libs mid-build
- **Name every socket event** — socket event naming inconsistencies between client/server are silent bugs
- **Describe the volume gotcha explicitly** — this caused a real production bug
- **Ask for migrations, not table drops** — always say "wrap in try/catch, column-already-exists is OK"
- **Specify `module.exports` patterns** — named vs default exports is a common source of `is not a function` crashes
- **Request CSS variable names explicitly** — prevents the AI inventing colour values instead of using the system
- **For JSX edits, request Python string replacement** — sed with special characters destroys JSX
### Avoid
- Asking for partial implementations and filling in later — the AI will make different assumptions
- Letting the AI choose the DB (it will pick Postgres — SQLite is intentional here)
- Letting the AI choose the state management (it will add Redux/Zustand — contexts are intentional)
- Vague feature requests like "add notifications" — specify: socket-based for online users, push for offline, badge for all
CSS system: Google Sans font, --primary #1a73e8, dark mode on [data-theme="dark"].
GOTCHAS:
- help.md must be at backend/src/data/help.md (NOT /app/data — that path is
volume-mounted and will shadow baked-in files)
- auth.js must export { authMiddleware } as named export
- dark mode --text-tertiary must be exactly 6 hex digits: #606080
- last_message_user_id must be included in real-time setGroups update
- Use Python string replacement for JSX file edits, never sed
- self.navigator.setAppBadge (not navigator) inside service worker scope
```
```
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.