A role-scoped CRM for an Indian sales organisation — customers, pipeline, tasks and team performance in one place, with an AI analyst built in and a cinematic opening sequence on the front door.
Seeded and ready. The login screen has a one-click button for each, so a reviewer never has to type.
| Role | Password | Sees | |
|---|---|---|---|
| Admin | admin@trishul.com |
Admin@123 |
Everything, plus Employees, Settings and the AI Assistant |
| Supervisor | supervisor@trishul.com |
Super@123 |
Only their own records and those of their direct reports |
| Executive | user@trishul.com |
User@123 |
Only records assigned to them |
Sign in as each in turn — the dashboard counters, tables, board and reports all change, because scoping is enforced in the API, not the UI.
Prerequisites: Node.js 20.9+, PostgreSQL 14+ running locally.
# 1. Create the database
createdb revoras # or: psql -U postgres -c "CREATE DATABASE revoras;"
# 2. API
cd server
npm install
cp .env.example .env # defaults already match the setup below
npm run migrate # 7 sequential migrations
npm run seed # 45 customers, 70 leads, 40 tasks, 9 users
npm run dev # http://localhost:5000
# 3. Web app — in a second terminal
cd client
npm install
cp .env.example .env.local
npm run dev # http://localhost:3000Open http://localhost:3000. The opening sequence plays once, then the login screen appears.
npm run db:reset in server/ rolls everything back and rebuilds from
scratch. The seed is deterministic, so a reset reproduces the same data.
server/.env
PORT=5000
DATABASE_URL=postgresql://postgres@localhost:5432/revoras
JWT_SECRET=thisisasecretkeyforjwt_tokengeneration
CLIENT_URL=http://localhost:3000
NODE_ENV=development
# Optional. Without it the assistant answers from SQL instead — see below.
ANTHROPIC_API_KEY=
AI_MODEL=claude-opus-5
client/.env.local
NEXT_PUBLIC_API_URL=http://localhost:5000/api
Both .env files are gitignored; .env.example is committed for each.
Dashboard — animated count-up counters, monthly lead flow, customer growth,
a pipeline donut, a live activity feed, latest customers / newest leads, and a
team leaderboard. Every figure comes from /api/reports/summary; nothing is
hardcoded.
Customers — searchable, filterable, sortable table with pagination; an animated create/edit modal; delete behind a confirmation; and a detail drawer showing the account's related tasks and its full audit trail.
Leads — a drag-and-drop Kanban across the five pipeline stages plus a table view. Cards move optimistically and roll back if the API rejects the change. Dropping onto Won opens the conversion dialog, because winning a lead is what creates the customer.
Tasks — list and board views. The list groups by urgency (overdue, due today, upcoming, completed) rather than by date, so what matters surfaces without filtering. Completing a task draws its tick and pulses a ring.
Employees (admin) — account CRUD, role and reporting-line assignment, inline activate/deactivate, and a recursive org chart with per-person workload.
Reports — date-range filtered, with lead volume, pipeline value, customers won, revenue added, source and task breakdowns, a leaderboard, and working Export PDF and Export Excel buttons. Both files carry the company name from Settings in the header.
AI Assistant (admin) — chat grounded in live CRM aggregates, streamed token by token over SSE.
Settings (admin) — company details, logo, currency, theme, password.
The backend gathers live aggregates — counts, pipeline, overdue work, top performers, inactive accounts — and passes them to Claude, streaming the reply back over Server-Sent Events.
When ANTHROPIC_API_KEY is absent, a deterministic local analyst answers
the same questions directly from SQL. All five suggested prompts are covered:
summarise today's activity, draft a follow-up email, write a proposal, list
inactive customers, and rank the team. It streams through the identical code
path, so the feature demos correctly offline, on a laptop, with no key — and
falls back automatically if the provider errors or declines.
The five-series palette was run through a colour-vision validator rather than picked by eye. Both themes pass every gate:
| Light | Dark | |
|---|---|---|
| OKLCH lightness band | ✅ | ✅ |
| Chroma floor | ✅ | ✅ |
| Worst adjacent CVD ΔE (target ≥ 8) | 15.9 | 16.0 |
| Worst normal-vision ΔE (floor ≥ 15) | 20.6 | 19.7 |
| Contrast vs surface (≥ 3:1) | ✅ | ✅ |
The dark column is the same five hues re-stepped for the dark surface, not an automatic flip. Counts and rupee values are never plotted on a shared axis.
trishul-crm/
├── client/ Next.js 16 · App Router · TypeScript strict
│ ├── public/trishul-mark.svg
│ └── src/
│ ├── app/
│ │ ├── page.tsx Opening sequence (once per session)
│ │ ├── login/
│ │ └── (app)/ Authenticated shell + route guard
│ │ ├── dashboard/ customers/ leads/ tasks/
│ │ └── employees/ reports/ ai-assistant/ settings/
│ ├── components/
│ │ ├── ui/ DataTable, Modal, Drawer, StatCard,
│ │ │ PageHeader, EmptyState, ConfirmDialog,
│ │ │ Toolbar, Pagination, Form, Badge, …
│ │ ├── charts/ Recharts wrappers with the house rules
│ │ ├── layout/ Sidebar, Topbar, AuthGate, transitions
│ │ ├── brand/ Inline-SVG trishul
│ │ ├── intro/ GSAP + tsparticles sequence
│ │ └── customers|leads|tasks|employees/
│ ├── hooks/ useResourceList, useCountUp, useDebounce…
│ ├── lib/ api (axios), export (PDF/Excel), markdown,
│ │ utils (currency, dates, text)
│ ├── store/ Zustand: auth, ui
│ └── types/
└── server/ Express 5 · ES modules
├── knexfile.js
└── src/
├── db/
│ ├── knex.js One shared instance
│ ├── migrations/ 7 sequential files
│ └── seeds/
├── middleware/ protect · authorize · scopeToRole · errors
├── routes/ → controllers/ → services/
├── validators/ express-validator chains
└── utils/ ApiError, asyncHandler, pagination, jwt
The layering rule: routes wire middleware, controllers stay thin, services
own the SQL. No route needs try/catch — an asyncHandler wrapper forwards
rejections to one centralized error middleware that returns a consistent
{ message, errors? }.
PostgreSQL + Knex, no ORM. The reporting queries — gap-filled monthly series, correlated per-employee aggregates, trigram search — are where the work is. A query builder expresses them directly; an ORM would mean fighting it. Lead conversion runs in a transaction, so a won lead can never point at a customer that was not created.
Role scoping in middleware, never the client. scopeToRole computes the
caller's visible record set and every service turns it into a WHERE clause.
The client-side guard only decides what to render. sortBy is checked
against a per-resource allow-list before it reaches Knex.
Tailwind v4 with one token file. Every colour, radius, shadow and easing
curve is declared once in globals.css and mapped onto utilities, so both
themes stay in step and no component invents its own value.
Zustand over a data-fetching library. Auth and UI state are small and
global; lists own their own state through one shared useResourceList hook
that also ignores superseded responses, so fast typing can't let a slow reply
overwrite a newer one.
All under /api. List endpoints accept ?page, ?limit, ?search,
?status, ?assignedTo, ?sortBy, ?order and return
{ data, pagination: { page, limit, total, totalPages } }.
| Method | Route | Notes |
|---|---|---|
POST |
/auth/login · /auth/logout |
JWT in an httpOnly cookie, header fallback |
GET PATCH |
/auth/me · /auth/profile · /auth/password |
|
GET POST PATCH DELETE |
/customers · /customers/:id |
Detail includes tasks + activity |
GET POST PATCH DELETE |
/leads · /leads/:id |
Plus /leads/board |
POST |
/leads/:id/convert |
Lead → customer, in one transaction |
GET POST PATCH DELETE |
/tasks · /tasks/:id |
Plus /board, /stats, /:id/complete |
GET POST PATCH DELETE |
/users · /users/:id |
Admin only; plus /hierarchy, /:id/supervisor |
GET |
/users/assignable |
Scope-limited; any role |
GET |
/reports/summary · /reports/analytics |
Role-scoped |
GET PATCH |
/settings |
Read: any role · Write: admin |
GET POST |
/ai/status · /ai/chat |
Admin only; SSE stream |
-
npm run migrate && npm run seedbuilds a populated database from scratch and prints the record counts. -
53 API checks pass against a freshly seeded database, covering the three demo logins, permission boundaries, search-filter-sort-paginate, the Kanban board, conversion and its double-conversion guard, validation shapes, reports, and all five AI prompts.
-
Role scoping measured against the seeded data — the same request returns a different slice for each account:
Signed in as Customers Leads Tasks Admin 45 70 40 Supervisor 24 34 20 Executive 6 8 5 -
npm run buildcompiles all 11 routes with TypeScript strict mode and noany. -
Both exports were downloaded from the running app and parsed back: the workbook opens with six sheets (Summary, Pipeline, Sources, Monthly, Team, Tasks) carrying the company name, period and scope in its header, and the PDF is a valid two-page document.
Not verified end to end: dragging a Kanban card was checked by code and by its keyboard/touch fallback rather than by a synthetic drag — Chrome does not raise native HTML5 drag events from automated input. Layouts were checked at desktop width in a real browser and rely on standard responsive utilities below that.
Frontend — Next.js 16 (App Router, TypeScript strict, src/, @/*),
Tailwind CSS v4, Motion, GSAP + tsparticles, Recharts, Zustand, Axios,
react-hook-form + zod, sonner, lucide-react, jsPDF + jspdf-autotable, SheetJS.
Backend — Node.js + Express 5 (ES modules), PostgreSQL with Knex.js, pg,
JWT + bcryptjs, cors, dotenv, cookie-parser, morgan, express-validator.

