Skip to content

Latest commit

 

History

25 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AI Chat Widget SaaS

Multi-tenant SaaS where organizations embed a chat widget on their websites that answers questions based on their uploaded documents (RAG).

Built with NestJS monorepo, Drizzle ORM, PostgreSQL + pgvector, BullMQ, OpenRouter.ai, and Cloudflare R2.

Tech Stack

Layer Choice
Runtime Node.js 24, TypeScript 6.0
Monorepo NestJS built-in (nest generate app)
API NestJS (Express)
ORM Drizzle
Database PostgreSQL 16 + pgvector
Queue BullMQ + Redis 7
Embeddings all-MiniLM-L6-v2 (Q8) via @huggingface/transformers
LLM Provider OpenRouter.ai
File Storage Cloudflare R2 (S3-compatible)
Auth JWT (dashboard) + API Keys (widgets)
Multi-tenancy Shared DB + organization_id column
Streaming SSE (Server-Sent Events)
Container Docker Compose (base + prod overrides)

Prerequisites

  • Docker + Docker Compose (for Postgres+pgvector + Redis)
  • Node.js 24

Quick Start

npm install
docker compose up -d            # start PG + Redis
npm run start:api:dev           # API on :3000
npm run start:worker:dev        # BullMQ worker

Commands

Command Description
npm run build api Build API app
npm run build worker Build Worker app
npm run start:api:dev API watch mode
npm run start:worker:dev Worker watch mode
npm run test Unit tests
npm run test:e2e:api API e2e tests
npm run test:e2e:worker Worker e2e tests

Project Structure

apps/api/          — HTTP API (NestJS)
apps/worker/       — BullMQ worker (NestJS, no HTTP)
libs/database/     — Drizzle schemas + client; shared queries (countReadyDocuments) (@app/database)
libs/queue/        — BullMQ config + job types (@app/queue)
libs/ai/           — OpenRouter client + local embeddings (@app/ai)
libs/storage/      — R2 client (@app/storage)
libs/config/       — @nestjs/config, env validation (@app/config)
libs/common/       — Guards, decorators, DTOs (@app/common)

API

Postman collection at postman_collection.json covering all endpoints.

Auth: POST /auth/register, POST /auth/login, GET/POST /auth/api-keys, DELETE /auth/api-keys/:id API keys are project-scoped: POST /auth/api-keys requires a projectId, and the key resolves its org/project on the widget face.

Users: GET /users/me, PATCH /users/me

Organizations: GET/POST /organizations, GET/PATCH/DELETE /organizations/:id, GET/POST /organizations/:id/members, PATCH/DELETE /organizations/:id/members/:userId

Projects: GET/POST /organizations/:orgId/projects, GET/PATCH/DELETE /organizations/:orgId/projects/:id — PATCH with { widget } updates the widget appearance config (free-form object).

Documents (JWT, project-scoped): organizations/:orgId/projects/:projectId/documents

  • GET / list (paged) · POST / multipart upload
  • POST /signed-url → presigned PUT URL (primary upload path)
  • POST /:id/complete → verify upload + enqueue ingest (idempotent)
  • GET /:id · DELETE /:id

Projects (JWT, org-scoped): organizations/:orgId/projects

  • GET /:id/events → SSE stream of project updates via Redis pub/sub on channel project:<projectId>. Emits event: ready-doc-count ({ count }, current number of ready documents) immediately on connect, then relays pubSub { event, data } payloads verbatim (currently event: document-status with { documentId, status, chunkCount?, readyDocumentCount }: processing, ready, failed). readyDocumentCount is computed by the worker on ready/failed; document deletes republish ready-doc-count with the post-delete count. Stays open until the client disconnects.

Chat widget face (API key, X-API-Key header):

  • POST /chat/conversations { sessionId } · GET /chat/conversations/:id/messages · POST /chat/conversations/:id/messages { content } → SSE stream

Conversations dashboard face (JWT): organizations/:orgId/projects/:projectId/conversations

  • POST / create · GET / list (paged, with message counts) · GET /:id detail + messages · GET /:id/messages · POST /:id/messages → SSE stream · DELETE /:id (admin)

Both message-SSE endpoints guard on the project's ready-document count (countReadyDocuments): if zero, they emit a single error SSE event and persist nothing.

Multi-Tenancy

Users ↔ Organizations (M:N via organization_users). Organizations have Projects. All scoped entities carry organization_id + project_id.

Conventions

  • All DB tables use organization_id for multi-tenancy (never tenant_id)
  • Drizzle for all DB access, TypeScript-native
  • Cloudflare R2 via S3 SDK (@aws-sdk/client-s3)
  • SSE for chat streaming
  • BullMQ for async RAG jobs
  • OpenRouter.ai for embeddings + chat
  • @app/* path aliases for all libraries
  • TypeScript 6.0, moduleResolution: nodenext

Phase Flow

Phases tracked in DESIGN_TODO.md. See AGENTS.md for the full design, schema, and implementation reference.

About

Multi-tenant AI Chat Widget SaaS

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages