Repository: github.com/sakthiii3/multi-tenant-task-manager
Git clone: https://github.com/sakthiii3/multi-tenant-task-manager.git
Author: Sakthi Sundaresan S · Contact: sakthisundaresan17@gmail.com
organization_id isolation in queries)| Layer | Technology |
|---|---|
| Frontend | React (Vite), hooks |
| Backend | Node.js, Express |
| Database | PostgreSQL |
| DevOps | Docker Compose |
Three services are defined in docker-compose.yaml:
| Service | Build / image | Host port | Purpose |
|---|---|---|---|
| db | postgres:16-alpine |
5432 | PostgreSQL; schema from backend/db/init.sql on first start |
| api | backend/Dockerfile |
4000 | Node.js + Express REST API |
| web | frontend/Dockerfile + nginx |
8080 | React static build; nginx proxies /auth, /tasks, /logs, /users, /health to api (same-origin browser calls) |
Clone and run:
git clone https://github.com/sakthiii3/multi-tenant-task-manager.git
cd multi-tenant-task-manager
docker compose up --build -d
localhost:5432, user mtms, password mtms_secret, database mtmsUseful commands:
docker compose ps
docker compose logs -f api
docker compose down # stop containers
docker compose down -v # stop and remove DB volume (fresh schema next up)
Optional: set JWT_SECRET or GOOGLE_CLIENT_ID in your environment before docker compose up (see docker-compose.yaml).
Live site: sakthiii3.github.io/multi-tenant-task-manager
GitHub Pages cannot run Node.js, PostgreSQL, or Docker — it only hosts static files. The workflow .github/workflows/github-pages.yml builds the React app and deploys it.
If you only see the README as a “webpage” (no real app), your repo is probably set to Deploy from a branch (e.g. /docs or default branch). Fix it:
main (or run the Deploy frontend to GitHub Pages workflow manually). The first run may ask you to approve the github-pages environment.Backend URL (required for login/tasks on the live site):
VITE_PUBLIC_API_URL — full base URL of your API, no trailing slash, e.g. https://mtms-api.onrender.comVITE_API_URL so the browser calls your real API.)CORS: your API’s CORS_ORIGIN must include https://sakthiii3.github.io (scheme + host only; no path).
Google sign-in: in Google Cloud Console, add Authorized JavaScript origins https://sakthiii3.github.io and set VITE_GOOGLE_CLIENT_ID as an Actions variable if you use it.
Production-style multi-tenant task app: PostgreSQL + Express + React, JWT auth (bcrypt passwords), RBAC (admin / member), activity logging, optional Google sign-in, Docker compose for one-command run.
| Role | Password | |
|---|---|---|
| Admin | admin@demo.local |
DemoPass123! |
| Member | member@demo.local |
DemoPass123! |
Load demo rows (optional):
From the project root, with Postgres running (e.g. docker compose up -d db):
# macOS / Linux / Git Bash
docker compose exec -T db psql -U mtms -d mtms < backend/db/seed.sql
# Windows PowerShell
Get-Content backend/db/seed.sql | docker compose exec -T db psql -U mtms -d mtms
Or with local psql: psql -f backend/db/seed.sql (set DATABASE_URL / connection flags as needed).
NITT/
├── docker-compose.yaml # Postgres + API + static UI (nginx)
├── SYSTEM_DESIGN.md # Architecture & scaling notes
├── README.md
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ ├── .env.example
│ ├── db/
│ │ ├── init.sql # Schema (run automatically in Docker on first DB start)
│ │ └── seed.sql # Optional demo org, users, and tasks
│ └── src/
│ ├── server.js
│ ├── app.js
│ ├── config/db.js
│ ├── middleware/ # auth, validation, errors
│ ├── routes/
│ ├── controllers/
│ ├── services/ # tenant + RBAC query rules
│ └── utils/
└── frontend/
├── Dockerfile
├── nginx.conf
├── vite.config.js
├── .env.example
└── src/
├── api/client.js
├── context/AuthContext.jsx
├── components/
└── pages/
Defined in backend/db/init.sql:
| Table | Purpose |
|---|---|
organizations |
Tenants |
users |
Users scoped to one org; role enum admin | member; optional Google oauth_provider / oauth_sub |
tasks |
organization_id, title, description, status, created_by, assigned_to |
activity_logs |
organization_id, task_id, action (CREATED / UPDATED / DELETED), performed_by, metadata, created_at |
Isolation: every service query includes organization_id from the verified JWT, never from unchecked client input.
RBAC: members see only tasks where created_by = current user; admins see all tasks in the org.
Base URL: http://localhost:4000 (or your deployed host).
/auth| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/register |
No | Register: either organizationName (creates org, you become admin) or organizationId (join existing org as member). Body: email, password (≥8), fullName, plus one org field. |
| POST | /auth/login |
No | Body: email, password, optional organizationId if the same email exists in multiple orgs. |
| GET | /auth/me |
Bearer JWT | Current user + organization. |
| POST | /auth/google |
No | Body: credential (Google ID token). Existing users: no extra fields. New users: mode: create + organizationName, or mode: join + organizationId. Requires GOOGLE_CLIENT_ID on server. |
/tasksAll require Authorization: Bearer <token>.
| Method | Path | Description |
|---|---|---|
| GET | /tasks |
List with pagination page, limit and filters status, assigned_to. |
| GET | /tasks/:id |
Single task (403/404 if outside tenant or RBAC). |
| POST | /tasks |
Create. Body: title, optional description, status, assigned_to. |
| PATCH | /tasks/:id |
Partial update. |
| DELETE | /tasks/:id |
Delete (activity log written first). |
/logs| Method | Path | Description |
|---|---|---|
| GET | /logs |
Paginated (page, limit), optional task_id filter. Scoped to org. |
/users| Method | Path | Description |
|---|---|---|
| GET | /users |
List users in the same organization (assignee picker / filters). |
| GET | /health |
{ "status": "ok" } — process is up (does not check the database). |
| GET | /health/ready |
{ "status": "ready", "database": true } or 503 with { "status": "not_ready", "database": false, "error": "..." } if Postgres is down or misconfigured. |
backend/Dockerfile — Node 20 Alpine, npm install --omit=dev, node src/server.jsfrontend/Dockerfile — Vite build + nginx static hostingdocker-compose.yaml — db (Postgres 16 + volume + init.sql), api, webEnvironment variables (see backend/.env.example):
DATABASE_URL, JWT_SECRET, PORT, CORS_ORIGIN, optional GOOGLE_CLIENT_IDFrom the project root (NITT/):
docker compose up --build
/auth, /tasks, /logs, /users, /health to the API, so login works the same whether you open http://localhost:8080 or http://127.0.0.1:8080.localhost:5432 (user mtms, password mtms_secret, DB mtms)After changing nginx or API proxy settings, rebuild the UI image: docker compose up -d --build web.
Override secrets:
set JWT_SECRET=your-long-random-secret
set GOOGLE_CLIENT_ID=your-google-web-client-id.apps.googleusercontent.com
docker compose up --build
(On Unix: export JWT_SECRET=....)
Start Postgres and create DB, or use Docker only for DB:
docker compose up db
Backend
cd backend
cp .env.example .env
# Edit .env — DATABASE_URL=postgresql://mtms:mtms_secret@localhost:5432/mtms
npm install
npm run dev
Frontend
cd frontend
cp .env.example .env
# Optional: VITE_GOOGLE_CLIENT_ID for Google button
npm install
npm run dev
Vite proxies /auth, /tasks, /logs, /users to localhost:4000, so you can leave VITE_API_URL empty for dev.
http://localhost:5173 (dev) and/or http://localhost:8080 (Docker UI).GOOGLE_CLIENT_ID (backend) and VITE_GOOGLE_CLIENT_ID (frontend .env).JWT_SECRET and DB passwords for any shared or production deployment.Secure, HttpOnly cookies for tokens in real production (this project uses localStorage + Bearer for simplicity)./auth and add refresh tokens if you extend the project.Provided as a sample / internship submission; adapt as needed.