jama 💬
just another messaging app
A modern, self-hosted team messaging Progressive Web App (PWA) built for small to medium teams. jama runs entirely in a single Docker container with no external database dependencies — all data is stored locally using SQLite.
Features
Messaging
- Real-time messaging — WebSocket-powered (Socket.io); messages appear instantly across all clients
- Image attachments — Attach and send images; auto-compressed client-side before upload
- Message replies — Quote and reply to any message with an inline preview
- Emoji reactions — Quick-react with common emojis or open the full emoji picker; one reaction per user, replaceable
- @Mentions — Type
@to search and tag users with autocomplete; mentioned users receive a notification - Link previews — URLs are automatically expanded with Open Graph metadata (title, image, site name)
- Typing indicators — See when others are composing a message
- Image lightbox — Tap any image to open it full-screen with pinch-to-zoom support
Channels & Groups
- Public channels — Admin-created; all users are automatically added
- Private groups / DMs — Any user can create; membership is invite-only by the owner
- Read-only channels — Admin-configurable announcement-style channels; only admins can post
- Support group — A private admin-only group that receives submissions from the login page contact form
Users & Profiles
- Authentication — Email/password login with optional Remember Me (30-day session)
- Forced password change — New users must change their password on first login
- User profiles — Custom display name, avatar upload, About Me text
- Profile popup — Click any user's avatar in chat to view their profile card
- Admin badge — Admins display a role badge; can be hidden per-user in Profile settings
Notifications
- In-app notifications — Mention alerts with toast notifications
- Unread indicators — Private groups with new unread messages are highlighted and bolded in the sidebar
- Web Push notifications — Badge and push notifications for mentions and new private messages when the app is backgrounded or closed (requires HTTPS)
Admin & Settings
- User Manager — Create, suspend, activate, delete users; reset passwords; change roles
- Bulk CSV import — Import multiple users at once from a CSV file
- App branding — Customize app name, logo, New Chat icon, and Group Info icon via the Settings panel
- Reset to defaults — One-click reset of all branding customizations
- Version display — Current app version shown in the Settings panel
PWA
- Installable — Install to home screen on mobile and desktop via the browser install prompt
- Dynamic app icon — Uploaded logo is automatically resized to 192×192 and 512×512 and used as the PWA shortcut icon
- Dynamic manifest — App name and icons in the PWA manifest update live when changed in Settings
- Offline fallback — Basic offline support via service worker caching
Contact Form
- Login page contact form — A "Contact Support" button on the login page opens a form (name, email, message, math captcha) that posts directly into the admin Support group
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Node.js, Express, Socket.io |
| Database | SQLite (better-sqlite3) |
| Frontend | React 18, Vite |
| Image processing | sharp |
| Push notifications | web-push (VAPID) |
| Containerization | Docker, Docker Compose |
| Reverse proxy / SSL | Caddy (recommended) |
Requirements
- Docker and Docker Compose v2
- A domain name with DNS pointed at your server (required for HTTPS and Web Push notifications)
- Ports 80 and 443 open on your server firewall (if using Caddy for SSL)
Building the Image
All builds use build.sh. No host Node.js installation is required — npm install and the Vite build run inside Docker.
# Build and tag as :latest only
./build.sh
# Build and tag as a specific version (also tags :latest)
./build.sh 1.0.0
# Build and push to Docker Hub
REGISTRY=yourdockerhubuser ./build.sh 1.0.0 push
# Build and push to GitHub Container Registry
REGISTRY=ghcr.io/yourorg ./build.sh 1.0.0 push
After a successful build the script prints the exact .env and docker compose commands needed to deploy.
Installation
1. Clone the repository
git clone https://github.com/yourorg/jama.git
cd jama
2. Build the Docker image
./build.sh 1.0.0
3. Configure environment
cp .env.example .env
nano .env
At minimum, change ADMIN_EMAIL, ADMIN_PASS, and JWT_SECRET. See Environment Variables for all options.
4. Start the container
docker compose up -d
# Follow startup logs
docker compose logs -f jama
On first startup you should see:
[DB] Default admin created: admin@yourdomain.com
[DB] Default jama group created
[DB] Support group created
5. Log in
Open http://your-server:3000 in a browser, log in with your ADMIN_EMAIL and ADMIN_PASS, and change your password when prompted.
HTTPS & SSL (Required for Web Push and PWA install prompt)
jama does not manage SSL itself. Use Caddy as a reverse proxy — it obtains and renews Let's Encrypt certificates automatically.
docker-compose.yaml (with Caddy)
version: '3.8'
services:
jama:
image: jama:${JAMA_VERSION:-latest}
container_name: jama
restart: unless-stopped
expose:
- "3000" # internal only — Caddy is the sole entry point
environment:
- NODE_ENV=production
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@jama.local}
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
- PW_RESET=${PW_RESET:-false}
- JWT_SECRET=${JWT_SECRET:-changeme}
- APP_NAME=${APP_NAME:-jama}
- JAMA_VERSION=${JAMA_VERSION:-latest}
volumes:
- jama_db:/app/data
- jama_uploads:/app/uploads
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
caddy:
image: caddy:alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_certs:/config
depends_on:
- jama
volumes:
jama_db:
jama_uploads:
caddy_data:
caddy_certs:
Caddyfile
Create a Caddyfile in the same directory as docker-compose.yaml:
chat.yourdomain.com {
reverse_proxy jama:3000
}
Prerequisites: Your domain's DNS A record must point to your server's public IP before starting Caddy, so the Let's Encrypt HTTP challenge can complete.
docker-compose.yaml Reference (without Caddy)
The default docker-compose.yaml exposes jama directly on a host port:
version: '3.8'
services:
jama:
image: jama:${JAMA_VERSION:-latest}
container_name: jama
restart: unless-stopped
ports:
- "${PORT:-3000}:3000" # change PORT in .env to use a different host port
environment:
- NODE_ENV=production
- ADMIN_NAME=${ADMIN_NAME:-Admin User}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@jama.local}
- ADMIN_PASS=${ADMIN_PASS:-Admin@1234}
- PW_RESET=${PW_RESET:-false}
- JWT_SECRET=${JWT_SECRET:-changeme_super_secret_jwt_key_2024}
- APP_NAME=${APP_NAME:-jama}
volumes:
- jama_db:/app/data # SQLite database
- jama_uploads:/app/uploads # avatars, logos, message images
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
jama_db:
driver: local
jama_uploads:
driver: local
Environment Variables
| Variable | Default | Description |
|---|---|---|
JAMA_VERSION |
latest |
Docker image tag to run. Set by build.sh or manually. |
ADMIN_NAME |
Admin User |
Display name of the default admin account |
ADMIN_EMAIL |
admin@jama.local |
Login email for the default admin account |
ADMIN_PASS |
Admin@1234 |
Initial password for the default admin account |
PW_RESET |
false |
If true, resets the admin password to ADMIN_PASS on every container restart. Shows a warning banner on the login page. For emergency access recovery only. |
JWT_SECRET |
(insecure default) | Secret used to sign auth tokens. Must be changed in production. Use a long random string. |
PORT |
3000 |
Host port to bind (only applies when not using Caddy's expose setup) |
APP_NAME |
jama |
Initial application name. Can also be changed at any time in the Settings UI. |
Note:
ADMIN_EMAILandADMIN_PASSare only used on the very first run to seed the admin account. Once the database exists these values are ignored — unlessPW_RESET=true.
Example .env
JAMA_VERSION=1.0.0
ADMIN_NAME=Your Name
ADMIN_EMAIL=admin@yourdomain.com
ADMIN_PASS=ChangeThisNow!
PW_RESET=false
JWT_SECRET=replace-this-with-a-long-random-string-at-least-32-chars
PORT=3000
APP_NAME=jama
First Login & Setup Checklist
- Open your app URL and log in with
ADMIN_EMAIL/ADMIN_PASS - Change your password when prompted
- Open ⚙️ Settings (bottom-left menu → Settings):
- Upload a custom logo
- Set the app name
- Optionally upload custom New Chat and Group Info icons
- Open 👥 User Manager to create accounts for your team
- Create public channels or let users create private groups
User Management
Accessible from the bottom-left menu (admin only).
| Action | Description |
|---|---|
| Create user | Set name, email, temporary password, and role |
| Bulk CSV import | Upload a CSV to create multiple users at once |
| Reset password | User is forced to set a new password on next login |
| Suspend | Blocks login; messages are preserved |
| Activate | Re-enables a suspended account |
| Delete | Removes account; messages remain attributed to user |
| Change role | Promote member → admin or demote admin → member |
CSV Import Format
name,email,password,role
John Doe,john@example.com,TempPass123,member
Jane Smith,jane@example.com,Admin@456,admin
rolemust bememberoradminpasswordis optional — defaults toTempPass@123if omitted- All imported users must change their password on first login
Group Types
| Public Channels | Private Groups | |
|---|---|---|
| Who can create | Admin only | Any user |
| Membership | All users (automatic) | Invite-only by owner |
| Visible to admins | ✅ Yes | ❌ No |
| Leave | ❌ Not allowed | ✅ Yes |
| Rename | Admin only | Owner only |
| Read-only mode | ✅ Optional | ❌ N/A |
Data Persistence
| Volume | Container path | Contents |
|---|---|---|
jama_db |
/app/data |
SQLite database (jama.db) |
jama_uploads |
/app/uploads |
Avatars, logos, PWA icons, message images |
Both volumes survive container restarts, image upgrades, and rollbacks.
Backup
# Backup database
docker run --rm \
-v jama_db:/data \
-v $(pwd):/backup alpine \
tar czf /backup/jama_db_$(date +%Y%m%d).tar.gz -C /data .
# Backup uploads
docker run --rm \
-v jama_uploads:/data \
-v $(pwd):/backup alpine \
tar czf /backup/jama_uploads_$(date +%Y%m%d).tar.gz -C /data .
Versioning, Upgrades & Rollbacks
jama uses a build-once, deploy-anywhere pattern:
Build machine Server
./build.sh 1.1.0 → JAMA_VERSION=1.1.0 → docker compose up -d
Upgrade
# 1. Build new version
./build.sh 1.1.0
# 2. Update .env
JAMA_VERSION=1.1.0
# 3. Redeploy (data volumes untouched)
docker compose up -d
Rollback
# 1. Set previous version in .env
JAMA_VERSION=1.0.0
# 2. Redeploy
docker compose up -d
PWA Installation
HTTPS is required for the browser install prompt to appear.
| Platform | How to install |
|---|---|
| Android (Chrome) | Tap the install banner, or Menu → Add to Home Screen |
| iOS (Safari) | Share → Add to Home Screen |
| Desktop Chrome/Edge | Click the install icon (⊕) in the address bar |
After uploading a custom logo in Settings, the PWA shortcut icon updates automatically on the next app load.
PW_RESET Flag
Setting PW_RESET=true resets the default admin password to ADMIN_PASS on every container restart. Use only for emergency access recovery.
When active, a ⚠️ warning banner is shown on the login page and in the Settings panel.
Always set PW_RESET=false and redeploy after recovering access.
Development
# Start backend (port 3000)
cd backend && npm install && npm run dev
# Start frontend in a separate terminal (port 5173)
cd frontend && npm install && npm run dev
The Vite dev server proxies all /api and /socket.io requests to the backend automatically.
License
MIT