A minimal multi-tenant task management system built with React (Vite), Node.js (Express), PostgreSQL (Sequelize), and Bull (Redis).
- Schema-based multitenancy: Each organization gets its own PostgreSQL schema. Tenant data is fully isolated — no row-level filtering needed.
- Background exports: Bull + Redis queue processes CSV exports asynchronously.
- Auth simulation: Pass
x-tenant-idandx-user-idheaders per request.
- Node.js 18+
- PostgreSQL 14+
- Redis 6+
git clone <repo-url>
cd task-manager
# Backend
cd backend
cp .env.example .env # Edit DB/Redis credentials
npm install
# Frontend
cd ../frontend
npm installEdit backend/.env:
PORT=3001
DB_HOST=localhost
DB_PORT=5432
DB_NAME=taskmanager
DB_USER=postgres
DB_PASSWORD=yourpassword
REDIS_URL=redis://localhost:6379
EXPORTS_DIR=./exports-- In psql:
CREATE DATABASE taskmanager;The public.organizations table is auto-created on server start.
brew install redis
brew services start redissudo apt-get install redis-server
sudo systemctl start redisdocker run -d -p 6379:6379 redis:7-alpinecd backend
npm run dev # or: npm start
# Server: http://localhost:3001cd frontend
npm run dev
# UI: http://localhost:5173Use the REST API to create an organization (tenant). This also creates its PostgreSQL schema and syncs models.
curl -X POST http://localhost:3001/organizations \
-H "Content-Type: application/json" \
-d '{"name": "Acme Corp", "schemaName": "acme"}'Response includes the organization id — save it as your TENANT_ID.
# Create an admin user
curl -X POST http://localhost:3001/organizations/<TENANT_ID>/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@acme.com", "role": "admin"}'
# Create a member user
curl -X POST http://localhost:3001/organizations/<TENANT_ID>/users \
-H "Content-Type: application/json" \
-d '{"name": "Bob", "email": "bob@acme.com", "role": "member"}'Save the user id values as ADMIN_ID and MEMBER_ID.
All tenant-scoped routes require:
x-tenant-idheader — Organization UUIDx-user-idheader — User UUID (required for export, optional for task listing)
| Method | Endpoint | Description |
|---|---|---|
| GET | /tasks |
List all tasks for tenant |
| POST | /tasks |
Create a task |
| PATCH | /tasks/:id |
Mark task as completed |
| POST | /tasks/export |
Trigger CSV export (admin only) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /exports/:id |
Get export status + file URL |
| GET | /exports/:id/download |
Download the CSV file |
| Method | Endpoint | Description |
|---|---|---|
| GET | /organizations |
List all organizations |
| POST | /organizations |
Create organization + schema |
| GET | /organizations/:id/users |
List users in tenant |
| POST | /organizations/:id/users |
Add user to tenant |
# 1. Set your IDs
TENANT_ID="<your-org-uuid>"
ADMIN_ID="<your-admin-uuid>"
# 2. Create some tasks first
curl -X POST http://localhost:3001/tasks \
-H "x-tenant-id: $TENANT_ID" \
-H "x-user-id: $ADMIN_ID" \
-H "Content-Type: application/json" \
-d '{"title": "Write tests", "description": "Unit + integration"}'
# 3. Trigger an export (admin only)
curl -X POST http://localhost:3001/tasks/export \
-H "x-tenant-id: $TENANT_ID" \
-H "x-user-id: $ADMIN_ID"
# Response: {"exportId": "<uuid>", "jobId": "1"}
EXPORT_ID="<export-uuid>"
# 4. Poll export status
curl http://localhost:3001/exports/$EXPORT_ID \
-H "x-tenant-id: $TENANT_ID" \
-H "x-user-id: $ADMIN_ID"
# When status = "completed", response includes fileUrl
# 5. Download the CSV
curl -O http://localhost:3001/exports/$EXPORT_ID/download \
-H "x-tenant-id: $TENANT_ID" \
-H "x-user-id: $ADMIN_ID"- Open
http://localhost:5173 - The top bar shows dropdowns for Tenant and User — the UI auto-loads available orgs/users from the API
- Tasks tab: view, create, and complete tasks
- Export tab (admins only): trigger an export and watch it update in real time; download the CSV when ready
task-manager/
├── backend/
│ ├── src/
│ │ ├── config/database.js # Sequelize connection
│ │ ├── middleware/tenant.js # Tenant resolution + user attach
│ │ ├── models/
│ │ │ ├── Organization.js # Public schema model
│ │ │ └── tenantModels.js # Per-schema User/Task/Export factories
│ │ ├── routes/
│ │ │ ├── tasks.js
│ │ │ ├── exports.js
│ │ │ └── organizations.js
│ │ ├── jobs/
│ │ │ ├── queue.js # Bull queue setup
│ │ │ └── exportWorker.js # Background CSV generation
│ │ └── index.js
│ ├── .env.example
│ └── package.json
└── frontend/
├── src/
│ ├── api/index.js # API helper functions
│ ├── pages/
│ │ ├── TasksPage.jsx
│ │ └── ExportPage.jsx
│ ├── App.jsx
│ └── main.jsx
├── index.html
├── vite.config.js
└── package.json
- Schema isolation:
getTenantModels(sequelize, schemaName)caches Sequelize model definitions per schema — no global model pollution - Dynamic schema creation:
POST /organizationsrunsCREATE SCHEMAand syncs models immediately - Bull + Redis: Export jobs are fire-and-forget; the worker updates
Export.statusand saves the CSV path on completion - No auth library: Auth is simulated via request headers as specified