A small, self-hosted, invite-only chat app for a family or friend group. Single Docker container (monolith) — no CORS, no microservices, no fuss.
- Frontend: Vue 3 + Vite + Pinia, installable as a PWA (offline shell)
- Backend: Python (FastAPI) + SQLite + WebSockets for real-time
- Joining: The first user is seeded as an admin on first boot. Everyone else joins with an invite code created by an existing member.
- Group chat + direct messages (real-time via WebSockets)
- Reactions, replies, edit & delete (own messages)
- Image / file attachments (≤10 MB) with a personal gallery + lightbox
- Typing indicators, read receipts (DMs), online status
- Profiles (avatar, name, bio, email, phone)
- Per-user notification settings (DND, mentions, replies)
- Invite management (create / copy / revoke, multi-use)
- PWA: install prompt, service worker, push-style notifications
- Dark theme, mobile responsive
# 1. Configure (optional) — copy the example and tweak
cp .env.example .env # set PORT, JWT_SECRET, ADMIN_PASSWORD
# 2. Build & run
docker compose up -d --build
# 3. Open the app (default port 8983)
open http://localhost:8983Log in with the seeded admin (admin / $ADMIN_PASSWORD). Create an invite in
Settings → Invite friends, share the code (or the /login?code=... link)
with family, and they join from the "Join with invite" tab.
Data (SQLite DB + uploaded files) persists in the
chat-dataandchat-uploadsDocker volumes. Stop/start the container freely — nothing is lost.
Local email (dev): the dev compose also runs a Mailpit email sink, so transactional email (invites, welcome, message notifications) is captured and viewable at http://localhost:3025 instead of leaving the machine. It is not part of the production stack.
The dev file (docker-compose.yml) is opinionated for local use (Mailpit sink,
localhost). Production uses docker-compose.prod.yml, which runs only the
app service against a real SMTP provider.
- Put the required values in
/chat/.envon the host (never commit them):JWT_SECRET=<openssl rand -hex 32> VAPID_PRIVATE_KEY=... # raw 32-byte base64url VAPID_PUBLIC_KEY=... # raw 65-byte uncompressed point, base64url VAPID_EMAIL=mailto:you@example.com APP_ORIGIN=https://your-domain # Real transactional SMTP provider (example: Resend) SMTP_HOST=smtp.resend.com SMTP_PORT=587 SMTP_USERNAME=resend SMTP_PASSWORD=<your provider SMTP API key> SMTP_FROM_ADDRESS=noreply@your-domain SMTP_FROM_NAME=Community Chat - Start it:
APP_GIT_HASH=$(git rev-parse --short HEAD) docker compose -f docker-compose.prod.yml up -d --build
The prod file uses :? defaults so it refuses to start if a core value is
missing (JWT secret, VAPID keys, APP_ORIGIN). The SMTP_* values are
optional — leave them empty to boot with email disabled, then add them and
recreate the container to turn it on.
backend/manage.py is a maintenance CLI for the operations the admin UI
performs, usable without a browser or a known admin password. It opens the app's
real database (the same DATA_DIR/DATABASE_URL the server uses), so it always
operates on the live data. Run it inside the container (prod) or from the
backend/ dir locally:
# Prod (from the host) — runs against the container's live DB
docker exec community-chat python manage.py list-members
# Local dev (from backend/)
python manage.py list-members --json| Command | What it does |
|---|---|
list-members |
Roster + activity stats (group/room counts, last active). --all includes inactive. |
delete-member <id|handle> |
Hard-delete a member and all their messages, reactions, and files. |
set-admin <id|handle> |
Grant/revoke the admin role (--role admin|member). |
create-invite |
Print a shareable invite code. --max-uses, --note, --family <name|id>. |
list-invites |
List invites. --all includes expired. |
revoke-invite <id> |
Revoke (or delete) an invite. |
list-families |
Families with member counts. |
create-family <name> |
Create a family (--description). |
assign-member <handle> <family|0> |
Put a user in a family; 0 removes them. |
reset-admin-password |
Recover the admin password (--handle, --password, or --generate). |
Notes:
--jsonon any command prints machine-readable output.- Destructive actions (
delete-member,revoke-invite, granting admin) prompt for confirmation; add--yesto skip (e.g. in a script). - Lookups accept a numeric id or a handle.
reset-admin-password --generateprints a new random password (the old one is gone — save the printed value).
If the seeded admin password is unknown (it's only logged once, on first boot):
docker exec community-chat python manage.py reset-admin-password --generateSet a specific one instead with --password '...' (or --handle if the admin
handle isn't admin).
# Backend (listens on PORT, default 8983)
cd backend
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
ADMIN_PASSWORD=devpass123 .venv/bin/python run.py # http://localhost:8983
# Frontend (separate terminal) — proxies /api and /ws to :8983
cd frontend
npm install
npm run dev # http://localhost:5173cd backend
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/python -m pytest tests/ -vTests run against a throwaway in-memory SQLite database (:memory:), so no
real data file is ever touched.
| Var | Default | Purpose |
|---|---|---|
PORT |
8983 |
Port the server listens on |
HOST |
0.0.0.0 |
Bind address |
ADMIN_HANDLE |
admin |
Seeded admin username |
ADMIN_PASSWORD |
(generated) | Seeded admin password (printed once) |
ADMIN_NAME |
Admin |
Seeded admin display name |
JWT_SECRET |
dev-…-me |
Token signing key — set in prod |
DATABASE_URL |
sqlite:///…/data/chat.db |
DB location |
DATA_DIR |
backend/data |
Where the SQLite file lives |
UPLOAD_DIR |
backend/uploads |
Where attachments are stored |
FRONTEND_DIR |
auto | Where the built frontend lives |
VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY / VAPID_EMAIL |
— | Web push (VAPID). Push is disabled without them. |
APP_ORIGIN |
http://localhost:8983 |
Public origin for deep links / VAPID audience (set to your domain in prod) |
SMTP_HOST |
(empty) | Transactional email SMTP host; empty = email disabled |
SMTP_PORT |
587 |
SMTP port (465 uses implicit TLS) |
SMTP_USERNAME / SMTP_PASSWORD |
(empty) | SMTP auth (some providers require it) |
SMTP_FROM_ADDRESS / SMTP_FROM_NAME |
(empty) | Sender address + name for outgoing email |
The seeder runs on startup and is idempotent — it only creates the admin if no user with that handle exists, so it never overwrites an existing account.
- Monolith: FastAPI serves the built Vue app (
index.html+ assets) and the/uploadsfiles, so the browser only ever talks to one origin — no CORS. - Real-time: a WebSocket hub fans out
message.new,dm.new,reaction.changed,typing, anddm.readevents to connected clients. - Single instance by design: there's intentionally no multi-tenancy. If you want a second community, run a second container with its own volumes.