A multi-tenant SaaS that turns uploaded invoices and receipts into structured, exportable data. Organizations sign up, invite teammates with roles, upload documents, and a background worker runs OCR-based extraction (date, vendor, total, line items). Results are reviewable in the UI and exportable as JSON or CSV. Every record is isolated by organization — a tenant can never read another tenant's data.
Built as a TypeScript Nx monorepo: a NestJS API, a standalone NestJS worker, and a Next.js (App Router) frontend, backed by PostgreSQL, Redis/BullMQ, and S3-compatible storage (MinIO locally).
flowchart LR
user([User]) --> web[web: Next.js]
web -->|REST + JWT| api[api: NestJS]
api -->|tenant-scoped reads/writes| db[(PostgreSQL)]
api -->|store upload| s3[(MinIO / S3)]
api -->|enqueue extraction job| redis[(Redis / BullMQ)]
redis -->|consume job| worker[worker: NestJS context]
worker -->|fetch object| s3
worker -->|OCR| tess[Tesseract]
worker -->|persist result + status| db
The API never does heavy work inline: it stores the upload, enqueues a job, and
returns. The worker consumes the queue, fetches the object, runs the swappable
Extractor (Tesseract today, an LLM implementation later), and writes the result
back with a status transition (pending → processing → extracted | failed). The
frontend polls document status to reflect progress.
apps/
api NestJS HTTP API — auth, organizations/invites/roles, documents, queue producer
worker Standalone Nest context — BullMQ consumer running the extraction pipeline
web Next.js App Router frontend
libs/
shared-types DTO / enum / queue contracts shared by api, worker, web
domain Tenant-scoped business rules (placeholder for growth)
config zod-validated environment loading
database Prisma client + module
storage S3-API storage service + module
extraction Extractor interface, Tesseract implementation, field parser
Prerequisites: Node 20+, npm, Docker.
npm install # also runs `prisma generate`
cp .env.example .env
docker compose up -d # Postgres + Redis + MinIO (+ bucket bootstrap)
npm run db:migrate # apply migrations
npm run db:seed # demo org, users, and documents
npm run serve:api # http://localhost:3000/api
npm run serve:worker # consumes extraction jobs
npm run dev:web # http://localhost:4200Health check: curl http://localhost:3000/api/health → {"status":"ok","db":"up"}.
After npm run db:seed, sign in at the web app with:
owner@demo.test/password123(owner of Acme Receipts, pre-populated documents)member@demo.test/password123(member of the same org)
Tasks run through Nx via npm scripts (local Nx, no npx).
| Task | Command |
|---|---|
| Serve api / worker | npm run serve:api · npm run serve:worker |
| Run web (dev) | npm run dev:web |
| Lint / typecheck / test (all) | npm run lint · npm run typecheck · npm test |
| Verify before commit | npm run verify |
| Build (all) | npm run build |
| Any other Nx target | npm run nx -- <target> <project> (e.g. npm run nx -- affected -t test) |
| One project's task | npm run nx -- test web |
Engineering conventions — code style, layering, central tenant isolation, the
extractor interface, and the Prisma-vs-TypeORM rationale — live in
CLAUDE.md. The /self-review, /architecture-check, and /verify
skills under .claude/skills/ enforce them.
Out of scope for the MVP, intentionally deferred:
- Billing and subscription plans
- Real-time collaboration and notifications
- The LLM
Extractorimplementation (the interface and DI seam are ready) - PDF rasterization for OCR (images are supported today)
- Real email delivery for invites (tokens are surfaced in the UI for now)
- Multi-language OCR and many additional document formats
- Webhooks and an audit log
- Training a custom extraction model