← all work
CS-01In build · India + KSA

GroundIT

A workforce platform for India and Saudi Arabia.

Role
Led a team of six
Timeline
2025 to now
Stack
NestJSNext.jsPostgreSQL + RLSDrizzleRedisKeycloakFlutterSerwist PWATensorFlow.jsOpenTelemetry

What it is

GroundIT covers everything that happens to an employee: pre-boarding, geofenced attendance, leave, payroll and tax, performance, ESOP, travel and expense, recruitment, exit. It runs in India and Saudi Arabia at the same time, so statutory work lives in country packs. EPFO, ESIC, TDS and professional tax for India. GOSI, WPS through Mudad and Saudization for KSA. Arabic RTL and Hijri calendars are in from the start, not bolted on.

I defined the product and led the team of six that built it. It is a pnpm monorepo with five deployable tiers: a NestJS API with 903 endpoints, a worker tier, a Next.js back-office with 188 screens, an offline-first PWA with 65 more, and a Flutter app. Underneath sits one Postgres database split into 23 schemas, one per domain module, with row-level security forced on every tenant-scoped table.

The part I am proudest of is the payroll engine. It lives in its own package, about 12k lines, and every payslip line it produces can explain itself: which formula ran, which statutory rule applied, what the inputs were. HR can run what-if simulations before committing a payroll cycle.

Architecture

clients
Next.js back-office · 188 screensOffline-first PWA · 65 screensFlutter app
api
NestJS modular monolith · 80 controllers · 903 endpoints
workers
Job registry + cronTransactional outboxPayslip PDFs · push
domain
pay-calc engine · what-if + explainCountry packs · IN / KSA
data
Postgres · 23 schemas · FORCE RLSRedis streamsMinIO
Five tiers, one monorepo. The control plane is a separate product.

Decisions that mattered

01

Schema per module, RLS forced at the database

Tenant isolation is not a WHERE clause someone remembers to write. Every module owns a Postgres schema, 23 in total, and row-level security is forced on tenant_id at the database layer. The test harness runs RLS-aware, so isolation is tested, not assumed. 161 migrations so far.

02

JavaScript budgets as tests, because our users carry cheap phones

Field staff use Android phones that cost around eight thousand rupees. So the test gate reads the build manifest and fails if any of the 64+ PWA routes ships more first-load JS than its budget. When the home route crept to 171.7 KB gzip, fixing sideEffects declarations brought it back to 149.7 KB. That fix is written up in the repo like everything else.

03

Offline first, with a real sync engine

Attendance punches can happen in a basement warehouse with no signal. The PWA queues writes in IndexedDB and drains them with idempotent replay, conflict handling and exponential backoff. Background sync picks up whatever the foreground missed.

04

Face-verified attendance without storing faces

Attendance pairs a geofence check with on-device face matching through TensorFlow.js. The server stores the verification result, never the raw biometrics. That is an explicit architecture decision record, one of 66 in the repo.

05

Payroll as a package, not a service method

The payroll engine is an independently testable package: formula evaluation, statutory rules, income tax, wage floors, final settlement. Country differences live in swappable compliance packs, so KSA payroll is configuration plus a pack, not a fork.

By the numbers

~809,000
lines of TypeScript and SQL
903
HTTP endpoints across 80 controllers
253
screens across web and PWA
23
Postgres schemas, RLS forced
161
versioned SQL migrations
943
test files
66
architecture decision records
2
countries with statutory packs