Tech Stack — Backend
Parent: Tech Stack Overview
Covers: apps/api, apps/servers/*, and all packages/ libraries.
Runtime & Language
| Technology | Version |
|---|---|
| Node.js (ESM) | v22 LTS |
| TypeScript | v5.x strict |
| pnpm workspaces | v10 |
| Turborepo | v2 |
| tsx (dev runner) | v4.19 |
All server processes run with tsx directly (no separate build step in development). Production builds transpile to JS via tsc.
API Server (apps/api)
| Technology | Version | Role |
|---|---|---|
| Fastify | v4.27 | Primary REST API, SSE, Socket.IO host |
| @fastify/cors | v9 | CORS — manual origin mirroring for SSE routes |
| @fastify/helmet | v11 | Security headers |
| @fastify/rate-limit | v9 | Rate limiting (allowList in test mode) |
| @fastify/multipart | v8 | File upload handling |
| @fastify/swagger | v8 + swagger-ui v4 | OpenAPI spec + Swagger UI |
| socket.io | v4.8 | Real-time peer-to-peer chat, notification delivery |
| @socket.io/redis-adapter | v8 | Pub/sub across multiple API instances |
Key patterns:
- Every route schema uses
{ type: "object", additionalProperties: true }to prevent silent field stripping - New routers registered in both
app.ts(test entry) andindex.ts(production entry) - SSE:
reply.raw.writeHead()bypasses@fastify/cors— origin mirrored manually instartSse() - Schemas live in
apps/api/src/schemas/—shared.ts+ domain files
Worker Servers (apps/servers/)
Seven standalone Node processes, each started by tsx src/index.ts. All consume BullMQ queues and share packages via the monorepo.
| Server | Queue(s) | Purpose |
|---|---|---|
agents | agent__* (one per agent role) | Hosts all BullMQ agent workers — website crawler, blog writer, social publisher, insight workers, etc. |
billing | internal | Invoice generation, payment job runner |
notifications | notification__dispatch | Email (SendGrid/SES/SMTP), push (FCM), WhatsApp, Telegram dispatcher |
ragengine | rag__ingest | Document embedding, Qdrant upsert, RAG ingestion pipeline |
reporting | report__generate | Custom report writer worker |
scheduler | — (DB-poll) | Polls ScheduledTask table; atomically claims and enqueues one-off jobs |
search-indexer | search__sync | Syncs 13 Typesense collections from PostgreSQL on change events |
Databases
| Database | Technology | Version | Use case |
|---|---|---|---|
| Relational | PostgreSQL | 16 | All domain data — tenants, users, activities, billing, channels, strategies, goals, content |
| Document | MongoDB | 7 | Audit logs only (via @leadmetrics/nosqldb) |
| Object storage | DigitalOcean Spaces / AWS S3 | @aws-sdk v3 | Blobs — crawled media, screenshots, PDFs, strategy docs, RAG files |
| Vector | Qdrant | latest | RAG embeddings + similarity search |
| Cache / Queue broker | Redis | 7-alpine | BullMQ job store, Socket.IO pub/sub, rate limiting |
ORM — Prisma v6.19:
| Concern | Location |
|---|---|
| Schema | packages/db/prisma/schema.prisma — single source of truth |
| Client generation | packages/db only — pnpm --filter @leadmetrics/db db:generate |
| DB push / migrations | packages/db only — pnpm --filter @leadmetrics/db db:push |
| Singleton export | packages/db/src/index.ts exports db |
| App import | import { db } from "@leadmetrics/db" — never new PrismaClient() |
MongoDB (@leadmetrics/nosqldb): Used exclusively for the audit log writer. connectMongo() is called automatically inside writeAuditLog() — no manual connection call needed.
S3 storage key convention:
webpages/{tenantId}/{channelId}/screenshots/{webPageId}.jpg
webmedia/{tenantId}/images/{contentHash}.{ext}
webmedia/{tenantId}/videos/{contentHash}.{ext}
webmedia/{tenantId}/documents/{contentHash}.{ext}
{tenantId}/rag/{datasetId}/{documentId}.{ext}Task Queue (BullMQ v5)
One shared queue per agent role (agent__{role}), used by all tenants. tenantId is in the job payload for isolation. Concurrency controlled per worker, not per queue.
Critical rules:
dedupeKeymust be cleared in bothcompletedandfailedhandlers — silent drop otherwise- Never create a separate
IORedisinstance inside workers — workers manage their own connections viagetRedisConnection() - Dynamic job IDs (timestamp-based) for revision chains — never static IDs that silently drop re-enqueues
Redis client: ioredis v5.3 — used directly in BullMQ workers and the API’s Socket.IO Redis adapter. 4 connections per API instance (Socket.IO emitter, BullMQ, rate-limit, session).
Authentication
| Technology | Version | Where used |
|---|---|---|
jsonwebtoken | v9.0 | API server (issue + verify), Dashboard server components |
jose | v6 | DM portal + Manage portal middleware (edge-compatible) |
@leadmetrics/middleware | workspace | createJwtAuthMiddleware() — used by all 3 Next.js portals |
JWT flow: 15-min access tokens + 7-day refresh tokens, both HS256. Fastify API is the single issuer (/auth/v1). Portals verify locally via middleware; expired access tokens are silently refreshed via POST /auth/v1/refresh.
Cookie names:
- Dashboard:
dashboard_access_token/dashboard_refresh_token - DM:
dm_access_token/dm_refresh_token - Manage:
manage_access_token/manage_refresh_token
LLM Providers
| Provider | Package | Version | Models |
|---|---|---|---|
| Anthropic Claude | @anthropic-ai/sdk | v0.74 | Sonnet 4.6, Opus 4.6, Haiku 4.5 |
| OpenAI | openai | v4 | GPT-4o, GPT-4o-mini, DALL-E 3 |
| Claude local | adapter-claude-local | workspace | Claude Code CLI child-process |
| Codex local | adapter-codex-local | workspace | OpenAI Codex via local process |
| Gemini local | adapter-gemini-local | workspace | Gemini via local process |
All adapters implement a common AgentAdapter interface. Model selection is per AgentConfig.adapter in the DB. ANTHROPIC_API_KEY must be in apps/servers/agents/.env.
RAG embedding / reranking: Handled in apps/servers/ragengine via @leadmetrics/provider-qdrant.
Real-Time
| Technology | Package | Purpose |
|---|---|---|
| Socket.IO | socket.io v4.8 + fastify-socket.io v5 | Peer-to-peer chat, real-time notifications on Dashboard |
| Redis adapter | @socket.io/redis-adapter v8 | Scales Socket.IO across multiple Fastify instances |
| SSE | Native Fastify reply.raw | Agent output streaming, live activity status |
Search
| Technology | Package | Purpose |
|---|---|---|
| Typesense | provider-typesense | Full-text search — 13 collections, Ctrl+K modal, all portals |
| Qdrant | provider-qdrant | Vector search — RAG knowledge base retrieval |
| Fuse.js | fuse.js v7.3 | In-browser fuzzy search — Help Center (packages/ui) |
Email & Notifications
| Technology | Package | Purpose |
|---|---|---|
| SendGrid | provider-sendgrid | Primary transactional email |
| AWS SES | provider-ses | Alternative transactional email |
| SMTP | provider-smtp | Generic SMTP fallback |
| Resend | resend v4 | Newsletter sending (in packages/agents) |
| Handlebars | v4.7 | Email template engine in apps/servers/notifications |
| Firebase FCM | provider-firebase | Web push notifications |
| WhatsApp Business | provider-whatsapp | WhatsApp notification channel |
| Telegram Bot | provider-telegram | Telegram notification channel |
All notification sends go through enqueueNotification() → BullMQ → apps/servers/notifications worker. Dev filter: DEV_ALLOWED_EMAIL_DOMAINS=leadmetrics.ai.
Payments
| Technology | Package | Purpose |
|---|---|---|
| Razorpay | provider-razorpay | Subscriptions, one-time payments, webhooks |
Webhook handler: POST /admin/v1/billing/webhook — HMAC-verified.
File Processing
| Technology | Package | Purpose |
|---|---|---|
| Playwright | playwright v1.44 | Website crawler (BFS crawl, screenshots, brand color/font extraction) |
| sharp | sharp v0.33 | Image compression for crawled media before DO Spaces upload |
| pdf-parse | pdf-parse v1.1 | Extract text from PDF documents for RAG ingestion |
| mammoth | mammoth v1.8 | Extract text from DOCX documents for RAG ingestion |
| marked | marked v18 | Markdown → HTML for PDF generation (via browser print API) |
Website Crawler (agent__tenant-web-crawler)
Triggered at signup when the tenant provides a website URL. Runs in apps/servers/agents.
- Fetches
robots.txt+sitemap.xmlto seed BFS queue - Playwright BFS up to
maxPages(default 100), same-origin only - Per page: stores
WebPage(title, text, contentHash), uploads screenshot, queues text for RAG - Per page: downloads images (compressed via sharp → DO Spaces), videos, documents →
WebMedia - Homepage only (
pageUrl === normalizedStart): runsextractBrandAssets()before DOM mutation — extracts CSS custom property colors, CTA/nav colors, Google Fonts links → writes tobrand_assetsifprimaryColoris currently null - On completion: sets
ConnectedChannel.isConnected = true
Brand extractor: packages/agents/src/utils/brand-extractor.ts
Observability & Logging
| Technology | Package | Purpose |
|---|---|---|
| Pino | pino v9 | Structured JSON logging across all server processes |
| createLogger | @leadmetrics/logger | Pino factory used by all packages — createLogger({ service }) |
Production log aggregation via Grafana Loki. Error monitoring for mobile via Sentry.
Testing
| Layer | Technology | Version | Scope |
|---|---|---|---|
| Unit | Vitest | v2.1 | Pure functions — prompt builders, validators, credit calc, utilities |
| Integration | Vitest + real DBs | v2.1 | API routes, BullMQ lifecycle — real PostgreSQL, no mocks |
| E2E | Playwright Test | v1.59 | Full user journeys across all three portals |
Key patterns:
vi.stubEnvfor env vars;vi.hoisted()for factories;hookTimeout: 30_000- Agent worker tests: export pure functions + standard 8-mock boilerplate
- Dashboard auth mock: mock
@/lib/server-auth, not@/lib/auth;requireSessionreturns{payload, user} - Server component tests: page.tsx queries Prisma directly — never fetch to Fastify
Key Environment Variables
# LLM
ANTHROPIC_API_KEY= # required in apps/servers/agents/.env
OPENAI_API_KEY=
# Databases
DATABASE_URL=postgresql://user:pass@postgres:5432/leadmetrics
MONGO_URL=mongodb://mongo:27017/leadmetrics
REDIS_URL=redis://redis:6379
QDRANT_URL=http://qdrant:6333
# Auth
JWT_SECRET= # shared across API + all portals
REFRESH_TOKEN_SECRET=
# Storage (DigitalOcean Spaces — all 6 required)
DO_SPACES_ENDPOINT=https://{region}.digitaloceanspaces.com
DO_SPACES_REGION=
DO_SPACES_BUCKET=
DO_SPACES_ACCESS_KEY_ID=
DO_SPACES_SECRET_ACCESS_KEY=
DO_SPACES_CDN_URL=
# Search
TYPESENSE_API_KEY=
TYPESENSE_HOST=
# Email
SENDGRID_API_KEY=
# Payments
RAZORPAY_KEY_ID=
RAZORPAY_KEY_SECRET=
RAZORPAY_WEBHOOK_SECRET=
# Internal
INTERNAL_API_SECRET= # API-to-API secret for server-to-server calls
DEV_ALLOWED_EMAIL_DOMAINS=leadmetrics.ai
DEV_CC_EMAIL=moble@leadmetrics.ai