Multi-tenant customer support platform — shared team inbox, realtime agent chat, and inbound/outbound email threading, built on an explicitly tenant-scoped Postgres schema.
Actively in development. Implemented and verified:
| Area | State |
|---|---|
| Multi-tenant schema (13 tables) | done |
| Auth, sessions, rotating refresh tokens | done |
| Tenant isolation + RBAC guards | done, cross-tenant probes return 403 |
| Conversations API (list, filter, reply, resolve) | done |
| Realtime layer (rooms, typing, presence, read receipts) | done |
| Email channel (IMAP inbound, SMTP outbound, threading) | done |
| Snooze sweeper | done |
Planned: knowledge base, AI summarization, embeddable widget, custom domains.
- Server — Node, Express 4, TypeScript ESM, raw SQL over
pg, Socket.io, Postgres 16 - Web — Vite, React 18, TypeScript, Tailwind, TanStack Query
- Deploy — Neon (Postgres), Render (API + realtime), Vercel (dashboard)
Raw SQL over an ORM. Keeps tenant scoping explicit and reviewable at every call site, makes Postgres full-text search straightforward, and avoids shipping an ORM engine binary onto a constrained tier.
Tenant isolation. requireMember resolves membership from the :workspaceId
path param onto req.member. Handlers scope every query by
req.member.workspaceId and never read a workspace id from the body or query
string — so a forged payload cannot cross tenants.
Read state per side, not per message. agent_last_read_at and
contact_last_read_at live on the conversation. A read is one row update; a
read receipt is a timestamp comparison.
Inbound email idempotency. A unique index on
(workspace_id, email_message_id) is the guard. Providers retry webhooks, so a
duplicate is a silent no-op caught via Postgres 23505.
Email threading resolves In-Reply-To, then walks References, then falls
back to subject matching against open conversations for the same contact within
30 days, before creating a new thread.
Presence uses in-process maps — correct for a single instance. Horizontal scale requires the Socket.io Redis adapter and shared presence state.
docker compose up -d postgres # Postgres on port 5433
npm run dev --workspace=@helpdesk/server # migrations run on bootEMAIL_PROVIDER=console logs the full envelope instead of sending, so the email
pipeline is testable with no credentials. See .env.example for configuration.