Tech stack and architecture
Valere CRM · Design foundations

General

Tech stack and architecture

Two deployables in one repository: a GraphQL API with its background worker, and a React web app. PostgreSQL is the single source of truth; everything a screen shows is read from it live.

Source: docs/ARCHITECTURE.md, the package.json files of api/ and web/, docker-compose.yml. Versions as of 2026-10-06.

The system at a glance

The browser talks only to the API. The API owns the database. Redis holds no records. All five parts run in the local stack; what each really does today is listed under the diagram.

CRM web app React 19 · Vite 8 Apollo Client 4 · Tailwind 4 runs in the browser API NestJS 11 · GraphQL Prisma 7 one module per domain area PostgreSQL 16 the single source of truth for every record Redis 7 job queue · query cache holds no records Worker NestJS · BullMQ 6 · background jobs GraphQL over HTTP JWT in the header SQL through Prisma cache, enqueue jobs

Technologies

BackendFor
NestJS 11The application framework
GraphQL, Apollo Server 5The API; the schema is generated from the code
Prisma 7Database access and migrations
PostgreSQL 16All domain data
PostGIS 3.4Geographic extension of PostgreSQL; switched on, not used by any table yet
Redis 7The job queue's storage; a query cache that is off in the local stack
BullMQ 6Background jobs in the worker; today only the password-reset email
JWT, bcryptSign-in: a 15-minute access token, a 7-day refresh token, hashed passwords
SentryError tracking; off on a local stack
FrontendFor
React 19The interface
Vite 8Dev server and build
TypeScript 7Types end to end
react-router 8Pages and URLs; filters and tabs live in the URL
Tailwind CSS 4Styling; the design tokens are its theme
Apollo Client 4GraphQL calls, cache, token refresh
GraphQL Codegen 7Typed queries from the API's schema
ToolingFor
Node 24.16The runtime, pinned in the Dockerfiles
pnpm 12Dependencies; api/ and web/ are separate workspaces
Docker ComposeThe local stack of five services
Jest 30, PGliteAPI tests on a real in-memory Postgres, no mocks
Oxlint, PrettierLinting and formatting

The path of a request

Every read and write crosses the same six steps.

Page React Apollo Client adds the token Resolver sign-in, role Assert service the record's rules Service reads, computes PostgreSQL through Prisma

One contract, generated types

The API's code is the source of the contract. The files with a dashed border are generated and never edited by hand.

API decorators resolvers, types, inputs schema.gql written when the API boots Typed documents GraphQL Codegen React pages typed queries Prisma models the data model Prisma Client used by the services Migrations applied before boot PostgreSQL tables and columns

Where things live

PathHolds
api/src/The backend, one folder per domain area: site, company, contact, activity, next-action, site-buyer, site-contractor, morning-meeting, dashboard, auth, staff-user and the platform modules
api/prisma/The data model and its migrations
api/test/End-to-end tests and their test database helper
web/packages/crm/The CRM web app: pages, the app shell, the session
web/shared/Shared components, the design tokens, the GraphQL documents and their generated types
docs/Architecture, glossary, runbook, decision records, one spec and plan per feature
initialDemo/This site

The local stack

One command, docker compose up -d --build, starts five services. Migrations and the demo data are applied before the API accepts requests.

ServiceRunsDefault port
web-crmThe web app's dev server5173
apiThe API, at /graphql3001
workerBackground jobs—
postgresPostgreSQL 16 with PostGIS5432
redisRedis 76379

Every host port can be moved with ./set-ports.sh <base>, so two copies of the stack can run side by side. Setup steps and known problems: docs/RUNBOOK.md.

Decisions that shape the code

Each has a decision record in docs/decisions/.

DecisionWhat it meansRecord
A Lead and an Opportunity are one rowA Lead becomes an Opportunity by a change of stage on the same Site. Nothing is copied.ADR-0019
The stage rules are a table in codeOrder, owning function and allowed moves are one constant, served to the web app.ADR-0017
Derived values are computed on readScores, flags and totals have no columns and no background jobs.ADR-0018, ADR-0024
Roles are checked by the APIThe web app locks what a role cannot change, but the API's checks are the rule.ADR-0016, ADR-0028
GraphQL first, schema from codeOne API surface; the web app's types are generated from it.ADR-0002, ADR-0008
Staff users are created by an adminThere is no sign-up; every account that can sign in holds one known role.ADR-0029