| client | ||
| cline_docs | ||
| server | ||
| .env | ||
| .gitattributes | ||
| .gitignore | ||
| docker-compose.dev.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
| stack.env | ||
| VideoCaptionsHD.mov | ||
Missoula Council of the Deaf, Inc. (MCDi) Website
Official website for MCDi at deafmissoula.org. Built on a full-stack TypeScript architecture with a MongoDB backend and a React frontend.
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript 5.8, Tailwind CSS, Framer Motion |
| Backend | Node.js 22 LTS, Express 5.1, TypeScript 5.8 |
| Database | MongoDB 7.0 (Mongoose 8.x ODM) |
| Auth | JWT (jose 5.x), argon2 password hashing |
| File uploads | multer 1.4.5-lts |
nodemailer (Google Workspace SMTP) |
|
| Containerization | Docker, Docker Compose |
| Reverse proxy | Caddy (on production server) |
Repository Structure
deafmissoula-website/
├── client/ # React frontend (Create React App)
│ ├── public/ # Static assets (photos, videos, PDFs, headshots)
│ └── src/
│ └── components/ # React components
├── server/ # Express backend
│ └── src/
│ ├── models/ # Mongoose models
│ ├── routes/ # API route handlers
│ ├── middleware/ # Auth, upload, error handling
│ ├── index.ts # Server entry point
│ ├── db.ts # MongoDB connection
│ └── seed.ts # Initial data seed script
├── docker-compose.yml # Production compose (2 containers: app + mongodb)
├── docker-compose.dev.yml # Dev override (no caddy_network requirement)
├── Dockerfile # Multi-stage build: client build + server compile
└── stack.env # Environment variables (committed — Gitea is internal-only)
API Routes
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/login |
— | Email + password login (rate limited: 5/5 min) |
| POST | /api/auth/forgot-password |
— | Send password reset email |
| POST | /api/auth/reset-password |
— | Set new password via reset token |
| POST | /api/auth/change-password |
temp JWT | Forced password change on first login |
| GET | /api/auth/me |
JWT | Current user info |
| GET/POST/PUT/DELETE | /api/board |
JWT (write) | Board members |
| PATCH | /api/board/reorder |
JWT | Drag-and-drop reorder |
| GET/POST/PUT/DELETE | /api/events |
JWT (write) | Calendar events |
| GET/POST/PUT/DELETE | /api/gallery |
JWT (write) | Photo gallery items |
| GET/POST/PUT/DELETE | /api/minutes |
JWT (write) | Meeting minutes |
| PATCH | /api/minutes/reorder |
JWT | Drag-and-drop reorder |
| GET/POST/PUT/DELETE | /api/sponsors |
JWT (write) | Sponsors |
| GET/POST/PUT/DELETE | /api/members |
JWT | Member directory |
| POST | /api/email |
— | Contact form submission |
Dashboard
Available at /manage (intentionally not /admin). Board members log in with their @deafmissoula.org email address.
First login flow: Board members are created with mustChangePassword: true. On first login they receive a temp JWT and are immediately redirected to set their own password.
Forgot password: Sends a 1-hour reset link to the member's email via Google Workspace SMTP.
New board member: When added via the dashboard, a welcome email is automatically sent with a 7-day set-password link.
Environment Variables (stack.env)
CACHEBUST=... # Increment to bust Docker layer cache
MONGO_INITDB_ROOT_USERNAME=mcdi
MONGO_INITDB_ROOT_PASSWORD=...
MONGO_INITDB_DATABASE=mcdi
MONGODB_URI=mongodb://mcdi:...@mongodb:27017/mcdi?authSource=admin
JWT_SECRET=...
ADMIN_PASSWORD=... # Initial password for admin@deafmissoula.org
APP_URL=https://deafmissoula.org # Used in password reset email links
Google email credentials live in .env (not committed):
GOOGLE_EMAIL=system@deafgain.org
GOOGLE_APP_PASSWORD=...
Local Development (Preview)
# Start MongoDB separately (or use docker compose)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# Seed the database (first run only)
cd server && npm run seed
# Dev servers
cd client && npm start # React dev server on :3000
cd server && npm run dev # Express with tsx watch on :801
Production Deployment
Production server: 10.4.0.205 (WireGuard VPN required)
Site path: /home/chaulmark/websites/deafmissoula.org/
ssh chaulmark@10.4.0.205
cd ~/websites/deafmissoula.org
git pull origin main
docker compose down
docker compose up -d --build
docker exec deafmissoulaorg-app-1 node dist/seed.js # First deploy only
Note on Caddy: The app container joins the external caddy_network, so Caddy can route to it by container name:
deafmissoula.org {
reverse_proxy deafmissoulaorg-app-1:801
}
If you see a 502 after a rebuild, check that the container name hasn't changed (docker ps).
Rollback to v1
The last hardcoded (no-database) version is tagged v1 in Git:
ssh chaulmark@10.4.0.205
cd ~/websites/deafmissoula.org
git checkout v1
docker compose down
docker compose up -d --build
v1 had no MongoDB container — docker compose down will cleanly remove the v2 stack. The MongoDB data volume (mcdi-mongo-data) is preserved unless you explicitly run docker compose down -v.
Docker Volumes
| Volume | Purpose |
|---|---|
mcdi-mongo-data |
MongoDB data (persists across rebuilds) |
mcdi-uploads-data |
User-uploaded files via dashboard (/uploads/) |
Static assets baked into the image (photos, videos, PDFs from client/public/) are served directly from client/build/ at their original paths (e.g. /photos/0001.jpg, /minutes/minutes-04122025.pdf). New uploads from the dashboard go to /uploads/headshots/, /uploads/photos/, etc.