Skip to content

Latest commit

 

History

History
63 lines (46 loc) · 2.48 KB

File metadata and controls

63 lines (46 loc) · 2.48 KB

helpdesk

Multi-tenant customer support platform — shared team inbox, realtime agent chat, and inbound/outbound email threading, built on an explicitly tenant-scoped Postgres schema.

Status

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.

Stack

  • 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)

Design decisions

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.

Local development

docker compose up -d postgres          # Postgres on port 5433
npm run dev --workspace=@helpdesk/server   # migrations run on boot

EMAIL_PROVIDER=console logs the full envelope instead of sending, so the email pipeline is testable with no credentials. See .env.example for configuration.