Skip to main content

Tech stack

The stack follows the team's existing conventions (lookin-hr, qmaks): npm workspaces, NestJS, Prisma, Vite + React 19 + Tailwind 4 + shadcn/ui + TanStack Query, Expo Router, Dokploy and GitHub Actions. The reasons for the bigger choices are recorded as ADRs in the decision log. How these pieces fit together is in System design; how they are deployed is in Deployment.

Version policy​

PolicyMeaning
LTS-pinnedUse the current LTS line. Move to a new LTS within 6 months of its release, after a staging soak.
Major-pinnedPin the major version in package.json (caret range). Renovate opens weekly PRs for minor and patch updates; they merge when CI is green. Majors are a planned task with an owner.
Exact-pinnedPin the exact version. Upgrade only through a reviewed PR with a changelog check. Used where a change can alter money, auth or data.
ManagedThe provider runs it. We choose the engine version and accept the provider's minor patching in a maintenance window.

All dependencies are locked by package-lock.json. The lockfile is committed, npm ci is used in CI, and Renovate groups updates by ecosystem. Production images are rebuilt weekly even without code changes, to pick up base-image security patches.

Backend and data​

LayerChoiceVersion policyWhyAlternatives considered
LanguageTypeScript (strict, noUncheckedIndexedAccess)Major-pinnedOne language across API, web, mobile and shared packages; shared zod contractsGo (fast, but a second language and no shared contracts); Kotlin/Java (heavier for a small team)
RuntimeNode.js 22 LTS on a distroless imageLTS-pinnedTeam skills; a stable LTS; small, non-root runtime imagesBun / Deno (less mature for NestJS and Prisma in production)
Monoreponpm workspaces (apps/*, packages/*)Exact-pinned npm (via packageManager)Team convention; no extra tool to learnpnpm workspaces; Nx. Decision pending: add Turborepo for build caching if CI exceeds 15 minutes.
API frameworkNestJS 11Major-pinnedModules, DI and guards map directly onto modular-monolith boundaries and the permission pipeline; team convention (ADR-001)Fastify alone (less structure); Express (no structure); tRPC (couples the client to TypeScript, which is poor for the public API)
HTTP adapterFastify adapter for NestJSMajor-pinnedLower overhead than Express; good schema supportExpress adapter (default, slower)
Contracts and validationzod schemas in packages/contractsMajor-pinnedOne schema validates the request on the server, types the client and generates OpenAPI 3.1class-validator DTOs (decorator duplication; no client sharing); TypeBox
OpenAPI generationGenerated from zod (zod's JSON Schema output plus a route registry)Major-pinnedThe spec cannot drift from the codeHand-written OpenAPI (it drifts); NestJS Swagger decorators (they duplicate zod). Decision pending: native zod JSON Schema versus zod-to-openapi.
ORMPrisma ORM with a tenant-scoped client extensionExact-pinnedTeam familiarity; type-safe queries; migrations (ADR-002)Drizzle (closer to SQL, attractive for RLS, but new to the team); Kysely; TypeORM (weaker typing)
Raw SQLPrisma $queryRaw with tagged templates, plus SQL views for reporting—Partitioned tables, RLS policies, window functions and FTS need SQL—
DatabasePostgreSQL 16+ on Amazon RDS (Multi-AZ, PITR)Managed; major upgrade yearlyRLS, JSONB, partitioning, FTS, pg_trgm, exclusion constraints for effective dating (ADR-002)MySQL (no RLS); MongoDB (weak relational integrity for payroll); Aurora (higher cost at our scale; revisit at Stage B)
Connection poolingPgBouncer in transaction modeExact-pinnedWorks with transaction-local set_config; cheapRDS Proxy. Decision pending: verify RDS Proxy pinning behaviour with set_config before considering it.
IDsUUIDv7 generated in the application (uuidv7 package)Exact-pinnedTime-ordered (B-tree friendly), globally unique across cells, safe for tenant movesBigint sequences (collide across cells); UUIDv4 (random index writes); ULID (text)
Cache, rate limits, queuesRedis 7 compatible (ElastiCache; Valkey acceptable)ManagedOne component covers caching, token buckets and BullMQMemcached (cache only); SQS (no per-tenant group concurrency)
Job queueBullMQMajor-pinnedDelays, repeatable jobs, flows (parent/child), rate limits; Node-nativepg-boss (Postgres-only, simpler, but less throughput); Temporal (powerful but heavy to operate). Decision pending: BullMQ Pro for per-tenant groups versus an open-source semaphore fallback.
Object storageAmazon S3 (ap-south-1) with SSE-KMS, versioning and Object LockManagedPresigned URLs, lifecycle rules, immutability for payroll artefactsMinIO (self-run; used only for local development)
SearchPostgreSQL FTS (tsvector, simple + unaccent) and pg_trgmwith PostgreSQLNo extra system; RLS applies automaticallyOpenSearch (deferred until the triggers in System design); Meilisearch
Money and decimalsInteger paise (bigint) + decimal.jsExact-pinnedExact arithmetic and explicit roundingFloats (banned); big.js (fewer rounding modes)
Dates and time zonesDecision pending: Temporal API polyfill versus date-fns + date-fns-tzExact-pinnedBusiness dates and zones must be explicitMoment (deprecated); Luxon
Policy and workflow expressionsDecision pending: JSONLogic versus a CEL subset for conditions; a small decimal-safe grammar for payroll formulasExact-pinnedSandboxed, side-effect-free evaluationJavaScript eval / vm (unsafe); a full rules engine (overkill)
Auth primitivesargon2 (argon2id), jose (JWT, ES256), openid-client (OIDC), otplib (TOTP), @node-saml/node-saml (SAML, Enterprise)Exact-pinnedSmall, audited libraries; we own the session model (ADR-006)Auth0 / Cognito / Clerk (per-MAU cost at SME price points, data residency, less control over company-code login); Keycloak (heavy to operate)
PDF generationHTML templates → PDF through a Gotenberg (headless Chromium) sidecar in workersMajor-pinnedTenant-customisable templates in HTML; good typography for payslips and letterspdfmake / @react-pdf/renderer (faster, but templates are code). Decision pending on throughput: about 1,000 payslips must render in under 5 minutes.
SpreadsheetsexceljsMajor-pinnedStreaming read and write for imports and exportsSheetJS (licence terms for some features)
TemplatingHandlebars in strict, sandboxed mode (no prototype access)Exact-pinnedNotification, letter and payslip templates editable by tenants (ADR-008)Liquid (fine; less familiar); MJML for email layout (may be added)
File scanningClamAV daemon in the worker clusterMajor-pinnedFree; good enough for documents and receiptsA commercial scanning API (cost)

Frontend (web)​

LayerChoiceVersion policyWhyAlternatives considered
Build toolViteMajor-pinnedFast development and builds; team conventionNext.js (SSR is not needed for an authenticated SPA; adds a server tier); webpack
UI libraryReact 19Major-pinnedTeam convention; ecosystemVue 3, Angular (team skills)
StylingTailwind CSS 4 with design tokens from packages/uiMajor-pinnedConsistent tokens shared with mobile; tenant branding through CSS variablesCSS Modules; styled-components (runtime cost)
Componentsshadcn/ui (Radix primitives, copied into the repo)Pinned per componentAccessible primitives that we own and can themeMUI, Ant Design (heavier and harder to theme)
Server stateTanStack QueryMajor-pinnedCaching, retries, invalidation, optimistic updatesSWR; Redux Toolkit Query
TablesTanStack TableMajor-pinnedHeadless; works with server-side cursor paginationAG Grid (licence and weight)
FormsReact Hook Form + zod resolverMajor-pinnedShares schemas with the APIFormik
RoutingDecision pending: React Router v7 (as in lookin-hr) versus TanStack RouterMajor-pinnedType-safe, lazy, role-aware routes—
ChartsRechartsMajor-pinnedSimple dashboardsECharts (heavier, more capable; candidate for R4 analytics)
i18ni18next + react-i18nextMajor-pinnedEnglish first; Hindi, Telugu and Tamil later; tenant label overridesFormatJS
Policy formsJSON-Schema-driven form renderer on shadcn/uiPinnedPolicy editors come from the same schema as the API (ADR-008)Hand-built forms for each policy (they drift)

Mobile​

LayerChoiceVersion policyWhyAlternatives considered
FrameworkExpo (managed workflow, prebuild for native modules)Upgrade within 3 months of each SDK releaseOne codebase for Android and iOS; EAS Build and Update; team convention (ADR-014)Bare React Native (more native toil); Flutter (second language); native apps (two codebases)
NavigationExpo RouterWith the Expo SDKFile-based, typed routes; role-based route groupsReact Navigation directly
Offline storageexpo-sqlite (offline action queue and caches)With the Expo SDKDurable, transactional queue for punches and visitsMMKV (key-value only); WatermelonDB (more than we need)
Secure storageexpo-secure-storeWith the Expo SDKKeychain and Keystore for tokensAsyncStorage (not secure)
Device capabilitiesexpo-camera, expo-location (+ expo-task-manager for R3 background tracking), expo-image-manipulator, expo-notificationsWith the Expo SDKSelfie punch, geo-fence, field tracking, pushThird-party background-geolocation SDK (decision pending for R3 if battery tests fail)
OTA updatesEAS Update with fingerprint-based runtimeVersionWith the Expo SDKJS fixes without a store review; safe binary compatibilityCodePush (retired)
Builds and submissionEAS Build and EAS Submit—Managed signing and store submissionFastlane on our own runners
Mobile E2EMaestroMajor-pinnedSimple YAML flows; works with ExpoDetox (more setup)

Platform, delivery and operations​

LayerChoiceVersion policyWhyAlternatives considered
ContainersDocker multi-stage builds; one apps/api image for the api, worker and ingest rolesBase image digest pinned; rebuilt weeklyOne artefact, three roles (ADR-001)Separate images per role (more pipeline work, no benefit yet)
Hosting, Stage ADokploy on 2× EC2 (ap-south-1, two AZs) + RDS + ElastiCache + S3 + CloudFrontDokploy minor updates monthlyExisting team practice; low cost (ADR-009)See the comparison in architecture.md
Hosting, Stage BECS Fargate per cell, defined by Terraform modulesTerraform and provider major-pinnedAutoscaling, per-cell isolation, less host toil (ADR-009)EKS (not justified below about 10 cells)
Dev, UAT and previewDokploy (per-PR preview environments with a seeded demo tenant)—Already in use; cheapVercel/Render previews (residency, split stack)
Infrastructure as codeTerraform (Stage B); Dokploy compose files in infra/ (Stage A)Major-pinnedReproducible cells; dedicated cells from one modulePulumi (less team familiarity); CDK
CI/CDGitHub ActionsActions pinned by commit SHATeam convention; matrix builds; OIDC to AWSGitLab CI; CircleCI
Registry and supply chainAmazon ECR (GHCR for previews), cosign signing, Trivy scanning, SBOM (Syft)Major-pinnedSigned, scanned images gate deployments (ADR-010)Docker Hub
Code qualityESLint (with eslint-plugin-boundaries), Prettier, dependency-cruiser, TypeScript project referencesMajor-pinnedEnforces module boundary rules B1–B9 in CINx module boundaries (needs Nx)
Security scanningCodeQL (SAST), npm audit + Renovate security alerts, gitleaksMajor-pinnedFree for our use; integrated with GitHubSnyk (cost)
Unit and integration testsVitest; Testcontainers (PostgreSQL, Redis) for integration and RLS isolation testsMajor-pinnedFast; real Postgres for RLSJest (slower; the NestJS default, replaced)
Web E2EPlaywrightMajor-pinnedReliable cross-browser tests; runs against preview environmentsCypress
Load testsk6Major-pinnedScripted month-end scenarios (punch storm, payroll run, payslip fan-out)Gatling, Artillery
ObservabilityOpenTelemetry SDK + Collector → Grafana (Tempo, Loki, Mimir/Prometheus); Sentry; pino logs with PII redactionMajor-pinnedVendor-neutral, tenant-tagged telemetry (ADR-012)Datadog (cost at scale); New Relic
Synthetic checksGrafana synthetic monitoring or Checkly—Login, punch and leave flows every 5 minutes per cellPingdom (shallow)
Feature flagsOpenFeature SDK with a control-plane providerMajor-pinnedFlags and entitlements in one place, with no extra vendor (ADR-004)LaunchDarkly, Unleash (cost, or another system to run)
SecretsAWS Secrets Manager and SSM Parameter Store; KMS for envelope encryptionManagedRotation, IAM-scoped access (ADR-007)Vault (operational load)
Edge and TLSCloudFront + AWS WAF; ACM wildcard certificates; Caddy on-demand TLS for Enterprise custom domains (Stage A)ManagedWildcard subdomains and automatic custom-domain certificates (ADR-005)Cloudflare (good; decision pending if WAF costs grow)
DocumentationDocusaurus (this site)Major-pinnedMarkdown docs next to the codeGitBook, Confluence

Third-party services​

CapabilityChoiceWhyAlternatives considered
EmailAmazon SES (ap-south-1)In-region, low costPostmark, SendGrid
SMSMSG91 or Gupshup (DLT-registered templates)India DLT compliance and pricingTwilio (cost, DLT friction)
WhatsAppMeta WhatsApp Cloud API through a BSPReach among Indian frontline workersDirect on-premise API (retired)
PushExpo push service (FCM and APNs)Works with EAS; one APIDirect FCM and APNs
Face verification (R2)Provider interface; first implementation AWS Rekognition (ap-south-1) or an on-device SDKLiveness + matching in-regionSelf-hosted models (accuracy and liveness risk). Decision pending after the privacy review.
Maps and geocodingProvider interface; Google Maps Platform or MapplsIndia coverage; snap-to-road for kmsOpenStreetMap + OSRM self-hosted (operations cost)
Subscription billingRazorpay Subscriptions (UPI AutoPay, cards, e-mandate)India payment methods; GST invoices (ADR-013)Stripe (limited India recurring support); Chargebee on top of Razorpay (later)

What we deliberately do not use (yet)​

Not usedReasonRevisit when
Microservices / service meshA modular monolith is enough (ADR-001)The extraction triggers fire for ingest or payroll compute
KubernetesOverhead not justified (ADR-009)About 10+ cells, or a multi-cloud requirement
KafkaOutbox + BullMQ covers our event volumeSustained more than about 5k events/s per cell, or CDC to a warehouse is needed
GraphQLREST + OpenAPI suits the public API and caching; screens use purpose-built endpointsMobile screens need many round trips that we can't fix with composite endpoints
A separate search clusterPostgreSQL FTS is enoughThe search triggers in System design
A data warehouseRead replicas and rpt_v_* views are enough for R1–R3Analytics workloads hurt OLTP, or R4 advanced analytics starts