Files
rosterchirp/Reference/jama-vibecode-prompt.md

31 KiB
Raw Blame History

jama — Complete Vibe-Code Build Prompt (v0.1.0)

How to use this document 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
Password hashing bcryptjs
Push notifications Web Push (web-push npm package, VAPID)
Image processing sharp (avatar/logo resizing)
Frontend framework React 18 + Vite
Frontend styling Plain CSS with CSS custom properties (no Tailwind, no CSS modules)
Frontend fonts Google Sans + Roboto (via Google Fonts CDN)
Emoji picker @emoji-mart/react + @emoji-mart/data
Markdown rendering marked (for help modal)
Container Docker multi-stage build (builder stage for Vite, runtime stage for Node)
Orchestration docker-compose.yaml with named volumes

Key npm packages (backend)

express, socket.io, better-sqlite3, bcryptjs, jsonwebtoken,
cookie-parser, cors, multer, sharp, web-push

Key npm packages (frontend)

react, react-dom, vite, socket.io-client, @emoji-mart/react,
@emoji-mart/data, marked

Part 3 — Project File Structure

jama/
├── Dockerfile                   # Multi-stage: Vite build → Node runtime
├── docker-compose.yaml          # Single service, two named volumes
├── build.sh                     # Docker build + optional push script
├── .env.example                 # All configurable env vars documented
├── about.json.example           # Optional About modal customisation
├── data/                        # Gitignored — runtime DB lives in Docker volume
├── backend/
│   ├── package.json
│   └── src/
│       ├── index.js             # Express app, Socket.io server, all socket events
│       ├── middleware/
│       │   └── auth.js          # JWT cookie auth middleware — exports { authMiddleware }
│       ├── models/
│       │   └── db.js            # Schema init, migrations, seed functions
│       ├── routes/
│       │   ├── auth.js          # Login, logout, register, change password
│       │   ├── users.js         # User CRUD, avatar upload, profile update
│       │   ├── groups.js        # Group CRUD, members, direct messages
│       │   ├── messages.js      # Message history, image upload
│       │   ├── settings.js      # App settings (name, logo, icons)
│       │   ├── push.js          # VAPID keys, subscription save, send helper
│       │   ├── about.js         # Serves about.json for About modal
│       │   └── help.js          # Serves help.md for Getting Started modal
│       ├── utils/
│       │   └── linkPreview.js   # Fetches og: meta for URL previews
│       └── data/
│           └── help.md          # Help content (NOT in /app/data volume)
└── frontend/
    ├── package.json
    ├── vite.config.js
    ├── index.html
    └── src/
        ├── main.jsx
        ├── App.jsx
        ├── index.css            # Global styles, CSS variables, dark mode
        ├── contexts/
        │   ├── AuthContext.jsx  # User state, login/logout
        │   ├── SocketContext.jsx # Socket.io connection, reconnect logic
        │   └── ToastContext.jsx # Global toast notifications
        ├── pages/
        │   ├── Login.jsx
        │   ├── Login.css
        │   ├── Chat.jsx         # Main app shell — groups state, socket events
        │   ├── Chat.css
        │   └── ChangePassword.jsx
        ├── components/
        │   ├── Sidebar.jsx      # Group/DM list, unread badges
        │   ├── Sidebar.css
        │   ├── ChatWindow.jsx   # Message list, typing indicator, header
        │   ├── ChatWindow.css
        │   ├── Message.jsx      # Individual message — reactions, reply, delete, link preview
        │   ├── Message.css
        │   ├── MessageInput.jsx # Text input, image upload, @mention, emoji
        │   ├── MessageInput.css
        │   ├── Avatar.jsx       # Circular avatar with initials fallback
        │   ├── GlobalBar.jsx    # Top bar (mobile only)
        │   ├── GroupInfoModal.jsx  # Group details, members, custom name
        │   ├── ProfileModal.jsx    # User profile, display name, avatar, about
        │   ├── SettingsModal.jsx   # Admin: app name, logo, icons, VAPID, users
        │   ├── UserManagerModal.jsx # Admin: user list, create, suspend, reset pw
        │   ├── NewChatModal.jsx    # Create group or start DM
        │   ├── HelpModal.jsx       # Getting Started modal (renders help.md)
        │   ├── AboutModal.jsx      # About this app modal
        │   ├── SupportModal.jsx    # Contact support (sends message to Support group)
        │   ├── ImageLightbox.jsx   # Full-screen image viewer
        │   └── UserProfilePopup.jsx # Click avatar → see profile, start DM
        └── utils/
            └── api.js           # All fetch calls to /api/*, parseTS helper
    └── public/
        ├── sw.js                # Service worker: cache, push, badge
        ├── manifest.json        # Static fallback (dynamic manifest served by Express)
        ├── favicon.ico
        └── icons/               # PWA icons: 192, 192-maskable, 512, 512-maskable, jama.png, logo-64.png

Part 4 — Database Schema

Use SQLite with WAL mode and foreign keys enabled.

-- Core tables (CREATE TABLE IF NOT EXISTS for all)

users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,                    -- login/real name
  email TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL,                -- bcrypt hash
  role TEXT NOT NULL DEFAULT 'member',   -- 'admin' | 'member'
  status TEXT NOT NULL DEFAULT 'active', -- 'active' | 'suspended'
  is_default_admin INTEGER DEFAULT 0,
  must_change_password INTEGER DEFAULT 1,
  avatar TEXT,                           -- relative URL e.g. /uploads/avatars/x.webp
  about_me TEXT,
  display_name TEXT,                     -- user-set public display name (nullable)
  hide_admin_tag INTEGER DEFAULT 0,
  last_online TEXT,                      -- datetime('now') string
  help_dismissed INTEGER DEFAULT 0,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
)

groups (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  type TEXT DEFAULT 'public',            -- 'public' | 'private'
  owner_id INTEGER REFERENCES users(id),
  is_default INTEGER DEFAULT 0,          -- 1 = General Chat (cannot be deleted)
  is_readonly INTEGER DEFAULT 0,
  is_direct INTEGER DEFAULT 0,           -- 1 = user-to-user DM
  direct_peer1_id INTEGER,               -- DM: first user ID
  direct_peer2_id INTEGER,               -- DM: second user ID
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
)

group_members (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  joined_at TEXT DEFAULT (datetime('now')),
  UNIQUE(group_id, user_id)
)

messages (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
  user_id INTEGER NOT NULL REFERENCES users(id),
  content TEXT,
  type TEXT DEFAULT 'text',              -- 'text' | 'system'
  image_url TEXT,
  reply_to_id INTEGER REFERENCES messages(id),
  is_deleted INTEGER DEFAULT 0,          -- soft delete
  link_preview TEXT,                     -- JSON string of og: meta
  created_at TEXT DEFAULT (datetime('now'))
)

reactions (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  emoji TEXT NOT NULL,
  created_at TEXT DEFAULT (datetime('now')),
  UNIQUE(message_id, user_id, emoji)     -- one reaction type per user per message
)

notifications (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  type TEXT NOT NULL,
  message_id INTEGER,
  group_id INTEGER,
  from_user_id INTEGER,
  is_read INTEGER DEFAULT 0,
  created_at TEXT DEFAULT (datetime('now'))
)

active_sessions (
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  device TEXT NOT NULL DEFAULT 'desktop', -- 'desktop' | 'mobile'
  token TEXT NOT NULL,
  ua TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  PRIMARY KEY (user_id, device)
)

push_subscriptions (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  endpoint TEXT NOT NULL,
  p256dh TEXT NOT NULL,
  auth TEXT NOT NULL,
  device TEXT DEFAULT 'desktop',
  created_at TEXT DEFAULT (datetime('now')),
  UNIQUE(user_id, device)
)

settings (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL,
  updated_at TEXT DEFAULT (datetime('now'))
)
-- Default settings keys: app_name, logo_url, pw_reset_active, icon_newchat,
-- icon_groupinfo, pwa_icon_192, pwa_icon_512, vapid_public, vapid_private

user_group_names (
  user_id INTEGER NOT NULL,
  group_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  PRIMARY KEY (user_id, group_id)
)
-- 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.

try {
  db.exec("ALTER TABLE users ADD COLUMN last_online TEXT");
} catch (e) { /* already exists */ }

Part 5 — Auth & Session System

  • JWT stored in HTTP-only cookie (token), 30-day expiry
  • authMiddleware in middleware/auth.js — verifies JWT, attaches req.user
  • Per-device sessions: one session per (user_id, device) pair — logging in on mobile doesn't kick out desktop
  • Device is detected from User-Agent: mobile if /Mobile|Android|iPhone/i.test(ua)
  • active_sessions table stores current tokens for invalidation on logout/suspend
  • must_change_password = 1 redirects to /change-password after login
  • Admin can reset any user's password; ADMPW_RESET=true env var resets default admin password on container start

Part 6 — Real-time Architecture (Socket.io)

Connection

  • Socket auth: JWT token in socket.handshake.auth.token
  • On connect: user joins group:{id} rooms for all their groups, and user:{id} for direct notifications
  • onlineUsers Map: userId → Set<socketId> (multiple tabs/devices)

Socket events (server → client)

Event Payload When
message:new full message object with reactions new message in a group
message:deleted { messageId, groupId } message soft-deleted
reaction:updated { messageId, reactions[] } reaction toggled
typing:start { userId, groupId, user } user started typing
typing:stop { userId, groupId } user stopped typing
notification:new { type, groupId, fromUser } mention or private message
group:updated group object group renamed, settings changed
group:removed { groupId } user removed from group

Socket events (client → server)

Event Payload
message:send { groupId, content, replyToId, imageUrl }
message:delete { messageId }
reaction:toggle { messageId, emoji }
typing:start { groupId }
typing:stop { groupId }
group:join-room { groupId }
group:leave-room { groupId }

Reconnect strategy (SocketContext.jsx)

// Aggressive reconnect for mobile PWA (which drops connections when backgrounded)
reconnectionDelay: 500,
reconnectionDelayMax: 3000,
timeout: 8000
// Also: on visibilitychange → visible, call socket.connect() if disconnected
// Also: on socket 'connect' event, reload groups (catches missed messages)

Part 7 — Push Notifications (Web Push / VAPID)

  • Generate VAPID keys once (stored in settings table as vapid_public / vapid_private)
  • Endpoint: POST /api/push/subscribe — saves subscription per (user_id, device)
  • Push is sent when the target user has no active socket connections (truly offline)
  • Push payload: { title, body, url, groupId }
  • groupId in payload enables per-conversation notification grouping via tag in showNotification
  • Service worker skips notification if app window is currently visible (already open)
  • Re-register push subscription on every visibilitychange → visible event (mobile browsers drop subscriptions when PWA is backgrounded)
  • navigator.setAppBadge(count) updates home screen badge (Chrome/Edge/Android — not iOS)

Part 8 — API Routes

All routes require authMiddleware except login/register/health.

Auth (/api/auth)

  • POST /login — returns JWT cookie, user object
  • POST /logout — clears cookie, removes active_session
  • POST /register — admin only
  • POST /change-password
  • GET /me — returns current user from JWT

Users (/api/users)

  • GET / — admin only: full user list with last_online
  • POST / — admin: create user
  • PATCH /:id — admin: update role/status/password
  • PATCH /me/profile — update own display_name, about_me, hide_admin_tag
  • POST /me/avatar — multipart: upload + sharp-resize to 200×200 webp
  • GET /check-display-name?name= — returns { taken: bool }

Groups (/api/groups)

  • GET / — returns { publicGroups, privateGroups } with last_message, peer data for DMs
  • POST / — create group or start DM (checks for existing DM between pair)
  • PATCH /:id — rename group
  • DELETE /:id — delete group (admin only, not default group)
  • POST /:id/members — add member
  • DELETE /:id/members/:userId — remove member
  • GET /:id/members — list members
  • POST /:id/custom-name — set per-user custom group name
  • DELETE /:id/custom-name — remove custom name

Messages (/api/messages)

  • GET /?groupId=&before=&limit= — paginated message history (50 per page)
  • POST /image — multipart image upload, returns { url }

Settings (/api/settings)

  • GET / — returns all settings (public: app_name, logo_url; admin: everything)
  • PATCH / — admin: update settings
  • POST /logo — admin: upload logo
  • POST /icon/:key — admin: upload PWA icon

Push (/api/push)

  • GET /vapid-public — returns VAPID public key (unauthenticated)
  • POST /subscribe — save push subscription

Help (/api/help)

  • GET / — returns rendered help.md content
  • GET /status — returns { dismissed: bool } for current user
  • POST /dismiss — set help_dismissed flag

Part 9 — Frontend Architecture

State management

No Redux. Three React contexts:

  1. AuthContextuser, login(), logout(), updateUser()
  2. SocketContextsocket instance, handles connection lifecycle
  3. ToastContexttoast(message, type) for notifications

Chat.jsx (main shell)

  • Holds groups state: { publicGroups[], privateGroups[] }
  • Handles all Socket.io group-level events and dispatches to child components
  • On message:new: updates last_message, last_message_at, last_message_user_id, re-sorts private groups newest-first
  • Tracks unreadGroups: Map<groupId, count> — increments on new messages in non-active groups (or active group when window is hidden)
  • Manages activeGroupId, notifications[], PWA badge count
  • 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)

--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: 0 1px 3px rgba(0,0,0,0.12);
--shadow-md: 0 2px 8px rgba(0,0,0,0.15);
--font: 'Google Sans', 'Roboto', sans-serif;

Dark mode ([data-theme="dark"])

--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)

# Stage 1: Build frontend
FROM node:20-alpine AS builder
WORKDIR /app
COPY frontend/package*.json ./frontend/
RUN cd frontend && npm install
COPY frontend/ ./frontend/
RUN cd frontend && npm run build

# Stage 2: Runtime
FROM node: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
EXPOSE 3000
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

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

  • Text messages with URL auto-linking
  • @mention with inline picker (type @ to trigger)
  • Image upload + display with lightbox
  • Link preview cards (og: meta, server-side fetch)
  • Reply-to with quoted preview
  • Emoji reactions (quick bar + full picker)
  • Message delete (soft delete — shows "deleted" placeholder)
  • Typing indicator with animated dots
  • Date separators
  • System messages (e.g. "User joined")
  • Emoji-only messages render at larger size
  • Infinite scroll (load older messages on scroll to top)

Groups & DMs

  • Public channels (# icon, auto-join for all users)
  • Private groups (lock icon, invite-only)
  • Read-only channels (📢, only admins can post)
  • User-to-user direct messages
  • Support group (private, all admins auto-added)
  • Per-user custom group display name (only visible to that user)
  • Group message list sorted newest-first (re-sorted on live messages)
  • DM shows peer's avatar and Display Name (real name) format

Users & Profiles

  • User display name (separate from login name)
  • User avatar (upload, resize to 200×200 webp)
  • About me text
  • Hide admin tag option
  • Last online timestamp in admin user list
  • Suspend/unsuspend users

Admin

  • Settings modal: app name, logo, PWA icons, VAPID key generation
  • User manager: create, edit, suspend, reset password
  • Admin can delete any message
  • Custom icons for UI elements (new chat button, group info button)

PWA / Notifications

  • Installable PWA (manifest, service worker, icons)
  • Web Push notifications (VAPID, works when app is closed)
  • App badge on home screen icon (Chrome/Edge/Android)
  • Page title unread count (N) App Name
  • Per-conversation notification grouping
  • Push re-registration on mobile resume (prevents subscription loss)

UX

  • Day / dark mode toggle (saved to localStorage)
  • Getting Started help modal (markdown, per-user dismiss)
  • About modal (customisable via about.json)
  • Support modal (sends message to Support group)
  • User profile popup (click any avatar)
  • Mobile-responsive layout (sidebar ↔ chat toggle)
  • 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

Iteration pattern that worked

  1. Session 1: Stack + schema + auth + basic messaging
  2. Session 2: Groups, DMs, reactions, replies
  3. Session 3: Push notifications, service worker, PWA manifest
  4. Session 4: Admin panel, user management, settings
  5. Session 5: Mobile fixes, reconnect logic, offline resilience
  6. Session 6+: Feature additions and bug fixes

Each session started by re-reading the previous session summary so the AI had full context on decisions made.


Part 16 — One-Shot Prompt (Copy-Paste to Start)

Use this if you want to try building jama in a single session. It is dense by design — the AI needs all of it.

Build a self-hosted team chat PWA called "jama". Single Docker container. 
No external services.

STACK: Node 20 + Express + Socket.io + SQLite (better-sqlite3) + JWT 
(HTTP-only cookie) + bcryptjs + React 18 + Vite. 
Push via web-push (VAPID). Images via multer + sharp.
Frontend: plain CSS with CSS custom properties, no Tailwind.

STRUCTURE:
- backend/src/index.js — Express app + all Socket.io events
- backend/src/middleware/auth.js — exports { authMiddleware }
- backend/src/models/db.js — schema, migrations (additive ALTER TABLE in try/catch only, never drop), seed
- backend/src/routes/ — auth, users, groups, messages, settings, push, about, help
- frontend/src/ — React app with AuthContext, SocketContext, ToastContext
- frontend/public/sw.js — service worker for push + badge + cache
- Dockerfile — multi-stage (Vite builder → Node runtime)
- docker-compose.yaml — single service, volumes: jama_db:/app/data, jama_uploads:/app/uploads

DATABASE TABLES: users (name, email, password, role, status, is_default_admin, 
must_change_password, avatar, about_me, display_name, hide_admin_tag, last_online, 
help_dismissed), groups (name, type, owner_id, is_default, is_readonly, is_direct, 
direct_peer1_id, direct_peer2_id), group_members, messages (content, type, image_url, 
reply_to_id, is_deleted, link_preview), reactions (UNIQUE message+user+emoji), 
notifications, active_sessions (PRIMARY KEY user_id+device), push_subscriptions 
(UNIQUE user_id+device), settings, user_group_names (PRIMARY KEY user_id+group_id).

AUTH: JWT in HTTP-only cookie. Per-device sessions (desktop/mobile detected from UA). 
must_change_password redirects to /change-password. ADMPW_RESET env var resets admin.

SOCKET EVENTS: message:new, message:deleted, reaction:updated, typing:start/stop, 
notification:new, group:updated, group:removed. Server tracks onlineUsers Map for 
push fallback. Reconnect: reconnectionDelay 500ms, re-register push on visibilitychange.

FEATURES: Public/private/readonly channels, user-to-user DMs, @mentions, emoji 
reactions (toggle — same emoji off, different emoji replaces), reply-to, image 
upload, link previews (server-side og: fetch), message soft-delete, typing indicator 
(bouncing dots — use CSS var(--text-tertiary) for dot colour), unread badges, 
page title (N) count, PWA badge via navigator.setAppBadge, Web Push with per-group 
notification tags, per-user custom group names, user display names, avatars (200x200 
webp), day/dark mode (CSS vars, saved localStorage), getting-started help modal 
(renders help.md via marked), admin settings (app name, logo, PWA icons, VAPID 
keygen), admin user manager, Support group (auto-created, all admins added).

LAYOUT: Desktop: 320px sidebar + flex chat. Mobile ≤768px: sidebar/chat toggle.
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