Rigi
The operations hub of a contact-centre software vendor, in English and Georgian. Client companies hold pools of licence seats synced from their own phone systems; their support requests arrive by email, become tasks priced in points against a free monthly allowance, and are billed on one monthly invoice together with the seats and any fixed-price projects. Five roles share the product: a superadmin, an accountant, a support desk, technicians, and a separate portal where the clients submit work, verify it, chat, dispute invoices and watch their allowance. We built it end to end — a NestJS API on Postgres with a job queue, and a Next.js front end — and this demo is that front end running against an in-browser mock, re-skinned as the fictional Rigi.
Year: 2026 · Category: business operations
Our role
Full-stack. The data model and billing rules, the job and mail pipeline, the role and tenant model, the invoice template renderer, and the web application with its five role areas.
The problem
A vendor selling contact-centre software to a dozen client companies was running its business out of spreadsheets, a shared mailbox and a folder of PDFs. Licences were counted by hand against what each client's phone system actually had. Support work was agreed over email and billed from memory. An invoice was assembled at month end from three places that did not agree.
Every one of those numbers depends on another. A seat is billed by the days a user held it; a support task costs points, the first few each month are free, and the rest are billed at the client's own rate in the client's own currency; a fixed-price project replaces the points for its tasks. If the invoice, the client's points page and the finance report each compute these separately, they drift — and the first client who disagrees with a total ends the conversation.
The approach
We made the money rules one pure module and built everything else around it. Partners, seats, users, tasks, points and projects are records; an invoice is derived from them by functions that the invoice screen, the points page, the reports and the seed all share. Around that sit a state machine for tasks, an inbox fed by IMAP that threads mail and links replies to the task they belong to, and an audit trail that records every change except the owner's.
The front end is role-first: four staff areas and a separate partner realm, each with its own shell, guarded by role, sharing one design system and one bilingual dictionary. For this demo we kept the front end and replaced the server with a mock that implements the same endpoints against an in-memory store, so every one of those rules runs in the visitor's browser.
Highlights
- One pure module owns the money: invoice lines, points, seat prices and totals are computed by the same functions for the screen, the reports and the seed.
- Seats are billed by the days they were held, and the pool is a history of batches, so changing the number of seats never rewrites the past.
- Points are debited when the client verifies a task, with the free monthly allowance applied inside the debit.
- The task state machine decides both what the server accepts and which buttons each role sees.
- Mail is threaded by reply headers, then by normalised subject, and a reply on a linked thread flags the task.
- The invoice template designer pairs a block builder with raw HTML and CSS and a signature pad, rendered by an in-house template engine that prints Georgian with no network.
- Clients live in a separate realm: every portal read is scoped to one company, and disabling a partner locks its admins out on their next request.
Architecture
- Repository: A pnpm workspace with three parts: the API, the web application, and a package of zod contracts that both sides import, so a request is validated against the same shape on the way out and on the way in.
- API: NestJS REST under a versioned prefix, documented with OpenAPI and consumed through a generated TypeScript client. A roles guard denies by default; request ids tie every log line to a call; a global audit interceptor records JSON-patch diffs, and deliberately never for the superadmin.
- Database: PostgreSQL 16 through Prisma, about forty models. Money is fixed-point decimal, tenancy is a partner id column on every client-owned table, and the constraints an ORM cannot express — partial unique indexes — are written in raw migration SQL.
- Jobs: pg-boss on the same database, scheduled in the vendor's own time zone: mail polling, month close, monthly invoice generation, a daily auto-draft for partners whose billing day is today, the daily user-count snapshot, and retention pruning.
- Auth: argon2id hashes; a fifteen-minute internal token plus a rotating opaque refresh token in an httpOnly cookie, bridged through the web app's route handlers. Partner admins live in a separate realm. Partner phone systems push user counts through machine credentials — a key id and secret exchanged for a short-lived, scope-limited token and throttled per minute.
- Web: Next.js 15 App Router with a route group per role, edge middleware that routes by role, server components for reads and route-handler proxies for mutations. shadcn/ui and Tailwind, with English and Georgian dictionaries.
- Mail: An IMAP poller stores each message and its attachments on a volume and threads them by RFC 5322 headers; SMTP carries password resets and invoice emails.
- PDF: Headless Chromium through Puppeteer renders editable HTML and CSS invoice templates, with a Georgian-capable font baked into the image.
- Infrastructure: Docker Compose for development and production: nginx with TLS in front of the API and web containers, a container that dumps the database for backups, health endpoints for liveness and readiness, an Ubuntu host.
- This demo: The web application is kept; the server is replaced by `@portfolio/demo-kit`. A mock API answers 163 endpoints in the browser, over a store built from a seed generated at load, with simulators for mail, chat replies and system health.
In numbers
Roles: 5 · Screens: 59 · Mocked endpoints: 163 · Data collections: 34 · Seeded partner users: 399 · Translation keys per language: 2,310 · Lines of TypeScript: 34k
Results
- One billing module feeds the invoice, the client's points page and both finance reports, so a figure reads the same wherever it appears.
- Support work is priced before it starts and billed once, when the client verifies it; the client sees the allowance left, and whether this task goes over it, before approving.
- A month-end invoice is a button: seats pro-rated by the days held, points beyond the allowance, fixed-price projects, VAT, and a PDF in the client's own template.
- Five roles see five different products from one codebase, and a client company can only ever reach its own rows.
- The whole interface — about 2,300 strings — exists in English and Georgian.
What is used
- Backend: NestJS, Prisma, zod contracts shared with the web app, OpenAPI with a generated TypeScript client, argon2id password hashing, JWT with rotating refresh tokens
- Data, mail and jobs: PostgreSQL 16, pgcrypto and citext, pg-boss job queue, IMAP polling, SMTP delivery, Puppeteer with headless Chromium
- Web: Next.js 15 App Router, React, TypeScript, Tailwind CSS, shadcn/ui, English and Georgian dictionaries
- Runtime and delivery: pnpm workspaces, Docker Compose, nginx with TLS, Scheduled database dumps, Ubuntu host
- Demo build: React 18, Vite 6, TypeScript, Tailwind CSS 4, TanStack Query 5, React Router 6, i18next, Recharts, dnd-kit, React Hook Form + zod, Radix UI, cmdk, framer-motion, sonner, @portfolio/demo-kit (in-browser mock API)
The demo
A fictional re-skin of a real product we built: Rigi, its client companies and its people are invented. There is no backend. A mock API in your browser answers all 163 endpoints from data generated when the page loads, relative to today, and everything you change lives in memory — reload and the tasks, seats and invoices are back as they were. Switch persona from the bar in the corner; mail arrives on a timer; email, PDF and partner-sync steps are simulated.