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
| Policy | Meaning |
|---|---|
| LTS-pinned | Use the current LTS line. Move to a new LTS within 6 months of its release, after a staging soak. |
| Major-pinned | Pin 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-pinned | Pin the exact version. Upgrade only through a reviewed PR with a changelog check. Used where a change can alter money, auth or data. |
| Managed | The 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
| Layer | Choice | Version policy | Why | Alternatives considered |
|---|---|---|---|---|
| Language | TypeScript (strict, noUncheckedIndexedAccess) | Major-pinned | One language across API, web, mobile and shared packages; shared zod contracts | Go (fast, but a second language and no shared contracts); Kotlin/Java (heavier for a small team) |
| Runtime | Node.js 22 LTS on a distroless image | LTS-pinned | Team skills; a stable LTS; small, non-root runtime images | Bun / Deno (less mature for NestJS and Prisma in production) |
| Monorepo | npm workspaces (apps/*, packages/*) | Exact-pinned npm (via packageManager) | Team convention; no extra tool to learn | pnpm workspaces; Nx. Decision pending: add Turborepo for build caching if CI exceeds 15 minutes. |
| API framework | NestJS 11 | Major-pinned | Modules, 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 adapter | Fastify adapter for NestJS | Major-pinned | Lower overhead than Express; good schema support | Express adapter (default, slower) |
| Contracts and validation | zod schemas in packages/contracts | Major-pinned | One schema validates the request on the server, types the client and generates OpenAPI 3.1 | class-validator DTOs (decorator duplication; no client sharing); TypeBox |
| OpenAPI generation | Generated from zod (zod's JSON Schema output plus a route registry) | Major-pinned | The spec cannot drift from the code | Hand-written OpenAPI (it drifts); NestJS Swagger decorators (they duplicate zod). Decision pending: native zod JSON Schema versus zod-to-openapi. |
| ORM | Prisma ORM with a tenant-scoped client extension | Exact-pinned | Team familiarity; type-safe queries; migrations (ADR-002) | Drizzle (closer to SQL, attractive for RLS, but new to the team); Kysely; TypeORM (weaker typing) |
| Raw SQL | Prisma $queryRaw with tagged templates, plus SQL views for reporting | — | Partitioned tables, RLS policies, window functions and FTS need SQL | — |
| Database | PostgreSQL 16+ on Amazon RDS (Multi-AZ, PITR) | Managed; major upgrade yearly | RLS, 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 pooling | PgBouncer in transaction mode | Exact-pinned | Works with transaction-local set_config; cheap | RDS Proxy. Decision pending: verify RDS Proxy pinning behaviour with set_config before considering it. |
| IDs | UUIDv7 generated in the application (uuidv7 package) | Exact-pinned | Time-ordered (B-tree friendly), globally unique across cells, safe for tenant moves | Bigint sequences (collide across cells); UUIDv4 (random index writes); ULID (text) |
| Cache, rate limits, queues | Redis 7 compatible (ElastiCache; Valkey acceptable) | Managed | One component covers caching, token buckets and BullMQ | Memcached (cache only); SQS (no per-tenant group concurrency) |
| Job queue | BullMQ | Major-pinned | Delays, repeatable jobs, flows (parent/child), rate limits; Node-native | pg-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 storage | Amazon S3 (ap-south-1) with SSE-KMS, versioning and Object Lock | Managed | Presigned URLs, lifecycle rules, immutability for payroll artefacts | MinIO (self-run; used only for local development) |
| Search | PostgreSQL FTS (tsvector, simple + unaccent) and pg_trgm | with PostgreSQL | No extra system; RLS applies automatically | OpenSearch (deferred until the triggers in System design); Meilisearch |
| Money and decimals | Integer paise (bigint) + decimal.js | Exact-pinned | Exact arithmetic and explicit rounding | Floats (banned); big.js (fewer rounding modes) |
| Dates and time zones | Decision pending: Temporal API polyfill versus date-fns + date-fns-tz | Exact-pinned | Business dates and zones must be explicit | Moment (deprecated); Luxon |
| Policy and workflow expressions | Decision pending: JSONLogic versus a CEL subset for conditions; a small decimal-safe grammar for payroll formulas | Exact-pinned | Sandboxed, side-effect-free evaluation | JavaScript eval / vm (unsafe); a full rules engine (overkill) |
| Auth primitives | argon2 (argon2id), jose (JWT, ES256), openid-client (OIDC), otplib (TOTP), @node-saml/node-saml (SAML, Enterprise) | Exact-pinned | Small, 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 generation | HTML templates → PDF through a Gotenberg (headless Chromium) sidecar in workers | Major-pinned | Tenant-customisable templates in HTML; good typography for payslips and letters | pdfmake / @react-pdf/renderer (faster, but templates are code). Decision pending on throughput: about 1,000 payslips must render in under 5 minutes. |
| Spreadsheets | exceljs | Major-pinned | Streaming read and write for imports and exports | SheetJS (licence terms for some features) |
| Templating | Handlebars in strict, sandboxed mode (no prototype access) | Exact-pinned | Notification, letter and payslip templates editable by tenants (ADR-008) | Liquid (fine; less familiar); MJML for email layout (may be added) |
| File scanning | ClamAV daemon in the worker cluster | Major-pinned | Free; good enough for documents and receipts | A commercial scanning API (cost) |
Frontend (web)
| Layer | Choice | Version policy | Why | Alternatives considered |
|---|---|---|---|---|
| Build tool | Vite | Major-pinned | Fast development and builds; team convention | Next.js (SSR is not needed for an authenticated SPA; adds a server tier); webpack |
| UI library | React 19 | Major-pinned | Team convention; ecosystem | Vue 3, Angular (team skills) |
| Styling | Tailwind CSS 4 with design tokens from packages/ui | Major-pinned | Consistent tokens shared with mobile; tenant branding through CSS variables | CSS Modules; styled-components (runtime cost) |
| Components | shadcn/ui (Radix primitives, copied into the repo) | Pinned per component | Accessible primitives that we own and can theme | MUI, Ant Design (heavier and harder to theme) |
| Server state | TanStack Query | Major-pinned | Caching, retries, invalidation, optimistic updates | SWR; Redux Toolkit Query |
| Tables | TanStack Table | Major-pinned | Headless; works with server-side cursor pagination | AG Grid (licence and weight) |
| Forms | React Hook Form + zod resolver | Major-pinned | Shares schemas with the API | Formik |
| Routing | Decision pending: React Router v7 (as in lookin-hr) versus TanStack Router | Major-pinned | Type-safe, lazy, role-aware routes | — |
| Charts | Recharts | Major-pinned | Simple dashboards | ECharts (heavier, more capable; candidate for R4 analytics) |
| i18n | i18next + react-i18next | Major-pinned | English first; Hindi, Telugu and Tamil later; tenant label overrides | FormatJS |
| Policy forms | JSON-Schema-driven form renderer on shadcn/ui | Pinned | Policy editors come from the same schema as the API (ADR-008) | Hand-built forms for each policy (they drift) |
Mobile
| Layer | Choice | Version policy | Why | Alternatives considered |
|---|---|---|---|---|
| Framework | Expo (managed workflow, prebuild for native modules) | Upgrade within 3 months of each SDK release | One 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) |
| Navigation | Expo Router | With the Expo SDK | File-based, typed routes; role-based route groups | React Navigation directly |
| Offline storage | expo-sqlite (offline action queue and caches) | With the Expo SDK | Durable, transactional queue for punches and visits | MMKV (key-value only); WatermelonDB (more than we need) |
| Secure storage | expo-secure-store | With the Expo SDK | Keychain and Keystore for tokens | AsyncStorage (not secure) |
| Device capabilities | expo-camera, expo-location (+ expo-task-manager for R3 background tracking), expo-image-manipulator, expo-notifications | With the Expo SDK | Selfie punch, geo-fence, field tracking, push | Third-party background-geolocation SDK (decision pending for R3 if battery tests fail) |
| OTA updates | EAS Update with fingerprint-based runtimeVersion | With the Expo SDK | JS fixes without a store review; safe binary compatibility | CodePush (retired) |
| Builds and submission | EAS Build and EAS Submit | — | Managed signing and store submission | Fastlane on our own runners |
| Mobile E2E | Maestro | Major-pinned | Simple YAML flows; works with Expo | Detox (more setup) |
Platform, delivery and operations
| Layer | Choice | Version policy | Why | Alternatives considered |
|---|---|---|---|---|
| Containers | Docker multi-stage builds; one apps/api image for the api, worker and ingest roles | Base image digest pinned; rebuilt weekly | One artefact, three roles (ADR-001) | Separate images per role (more pipeline work, no benefit yet) |
| Hosting, Stage A | Dokploy on 2× EC2 (ap-south-1, two AZs) + RDS + ElastiCache + S3 + CloudFront | Dokploy minor updates monthly | Existing team practice; low cost (ADR-009) | See the comparison in architecture.md |
| Hosting, Stage B | ECS Fargate per cell, defined by Terraform modules | Terraform and provider major-pinned | Autoscaling, per-cell isolation, less host toil (ADR-009) | EKS (not justified below about 10 cells) |
| Dev, UAT and preview | Dokploy (per-PR preview environments with a seeded demo tenant) | — | Already in use; cheap | Vercel/Render previews (residency, split stack) |
| Infrastructure as code | Terraform (Stage B); Dokploy compose files in infra/ (Stage A) | Major-pinned | Reproducible cells; dedicated cells from one module | Pulumi (less team familiarity); CDK |
| CI/CD | GitHub Actions | Actions pinned by commit SHA | Team convention; matrix builds; OIDC to AWS | GitLab CI; CircleCI |
| Registry and supply chain | Amazon ECR (GHCR for previews), cosign signing, Trivy scanning, SBOM (Syft) | Major-pinned | Signed, scanned images gate deployments (ADR-010) | Docker Hub |
| Code quality | ESLint (with eslint-plugin-boundaries), Prettier, dependency-cruiser, TypeScript project references | Major-pinned | Enforces module boundary rules B1–B9 in CI | Nx module boundaries (needs Nx) |
| Security scanning | CodeQL (SAST), npm audit + Renovate security alerts, gitleaks | Major-pinned | Free for our use; integrated with GitHub | Snyk (cost) |
| Unit and integration tests | Vitest; Testcontainers (PostgreSQL, Redis) for integration and RLS isolation tests | Major-pinned | Fast; real Postgres for RLS | Jest (slower; the NestJS default, replaced) |
| Web E2E | Playwright | Major-pinned | Reliable cross-browser tests; runs against preview environments | Cypress |
| Load tests | k6 | Major-pinned | Scripted month-end scenarios (punch storm, payroll run, payslip fan-out) | Gatling, Artillery |
| Observability | OpenTelemetry SDK + Collector → Grafana (Tempo, Loki, Mimir/Prometheus); Sentry; pino logs with PII redaction | Major-pinned | Vendor-neutral, tenant-tagged telemetry (ADR-012) | Datadog (cost at scale); New Relic |
| Synthetic checks | Grafana synthetic monitoring or Checkly | — | Login, punch and leave flows every 5 minutes per cell | Pingdom (shallow) |
| Feature flags | OpenFeature SDK with a control-plane provider | Major-pinned | Flags and entitlements in one place, with no extra vendor (ADR-004) | LaunchDarkly, Unleash (cost, or another system to run) |
| Secrets | AWS Secrets Manager and SSM Parameter Store; KMS for envelope encryption | Managed | Rotation, IAM-scoped access (ADR-007) | Vault (operational load) |
| Edge and TLS | CloudFront + AWS WAF; ACM wildcard certificates; Caddy on-demand TLS for Enterprise custom domains (Stage A) | Managed | Wildcard subdomains and automatic custom-domain certificates (ADR-005) | Cloudflare (good; decision pending if WAF costs grow) |
| Documentation | Docusaurus (this site) | Major-pinned | Markdown docs next to the code | GitBook, Confluence |
Third-party services
| Capability | Choice | Why | Alternatives considered |
|---|---|---|---|
Amazon SES (ap-south-1) | In-region, low cost | Postmark, SendGrid | |
| SMS | MSG91 or Gupshup (DLT-registered templates) | India DLT compliance and pricing | Twilio (cost, DLT friction) |
| Meta WhatsApp Cloud API through a BSP | Reach among Indian frontline workers | Direct on-premise API (retired) | |
| Push | Expo push service (FCM and APNs) | Works with EAS; one API | Direct FCM and APNs |
| Face verification (R2) | Provider interface; first implementation AWS Rekognition (ap-south-1) or an on-device SDK | Liveness + matching in-region | Self-hosted models (accuracy and liveness risk). Decision pending after the privacy review. |
| Maps and geocoding | Provider interface; Google Maps Platform or Mappls | India coverage; snap-to-road for kms | OpenStreetMap + OSRM self-hosted (operations cost) |
| Subscription billing | Razorpay 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 used | Reason | Revisit when |
|---|---|---|
| Microservices / service mesh | A modular monolith is enough (ADR-001) | The extraction triggers fire for ingest or payroll compute |
| Kubernetes | Overhead not justified (ADR-009) | About 10+ cells, or a multi-cloud requirement |
| Kafka | Outbox + BullMQ covers our event volume | Sustained more than about 5k events/s per cell, or CDC to a warehouse is needed |
| GraphQL | REST + OpenAPI suits the public API and caching; screens use purpose-built endpoints | Mobile screens need many round trips that we can't fix with composite endpoints |
| A separate search cluster | PostgreSQL FTS is enough | The search triggers in System design |
| A data warehouse | Read replicas and rpt_v_* views are enough for R1–R3 | Analytics workloads hurt OLTP, or R4 advanced analytics starts |