Spyrl — Architecture, Deployment & Multi-Client Management
Version: 1.0 · Date: 2026-10-31 · Status: Recommended baseline (ADR-001 to ADR-014)
Related: summary.md · prd.md (NFRs) · project-plan.md · review.md · CONVENTIONS.md
This document answers Phase 5: how Spyrl should be built, deployed, isolated, operated and scaled so that one small team can serve many client organisations reliably. Each decision lists the options considered, the recommendation, and the pros, cons and triggers for revisiting it.
1. Requirements that drive the architecture
| Driver | Source | Consequence |
|---|
| Many SME tenants (10–1,000 employees each), price-sensitive | BO-01, BO-04 in brd.md | Pooled infrastructure; low cost per tenant; self-serve onboarding |
| A few large or regulated clients demanding isolation | BR-PLT-05 | A dedicated option must exist without forking the code |
| Payroll and salary data, Aadhaar, PAN, bank, biometric and location data | NFR-SEC, NFR-PRV | Strong tenant isolation, encryption, audit, India data residency, consent |
| Month-end spikes (payroll runs, payslip emails, attendance computation) | NFR-PERF | Async job queue, horizontal workers, per-tenant fairness |
| Continuous GPS ingest from field users (R3) | FFM | A write-heavy path separable from the OLTP core |
| Statutory rules change by date and state | PAY | Effective-dated reference data; reproducible payroll runs |
| One release train, small team | BO-05 | Modular monolith, a single codebase, automated rollout |
2. System overview
2.1 Application plane: a modular monolith (ADR-001)
| Option | Pros | Cons |
|---|
| Microservices from day one | Independent scaling and deploys | Large operational overhead, distributed transactions (attendance → payroll), slow for a small team |
| Modular monolith (recommended) | One deployable, ACID across modules, fast delivery, easy local dev, strict boundaries are still possible | It scales as a unit, so boundaries need discipline |
| Serverless functions | Pay-per-use | Cold starts, hard local dev, long payroll jobs are awkward, vendor lock-in |
Decision: NestJS 11 (TypeScript) modular monolith. Module rules:
- Each module (
org, iam, emp, att, lve, pay, ntf, rpt, clm, ext, eng, ffm, tsm, lms) owns its tables. Other modules call its public service interface or react to its domain events; no cross-module table access. An ESLint boundary rule and a dependency-cruiser check run in CI.
- The same image runs in three roles, chosen by
PROCESS_ROLE=api|worker|ingest.
- Extraction candidates (only when the triggers fire):
ingest (GPS points > ~200 writes/s sustained) and the payroll-compute worker (a run > 10 minutes for the largest tenant).
spyrl/ # npm workspaces (matches team conventions in lookin-hr / qmaks)
├── apps/
│ ├── api/ # NestJS app plane (api | worker | ingest roles)
│ ├── control-plane/ # NestJS control-plane API
│ ├── web/ # Vite + React 19 SPA (tenant app)
│ ├── admin/ # Vite + React platform admin console
│ └── mobile/ # Expo Router app (ESS, MSS, field, kiosk)
├── packages/
│ ├── db/ # Prisma schema + migrations (app plane)
│ ├── cp-db/ # Prisma schema (control plane)
│ ├── contracts/ # zod schemas + OpenAPI types shared web/mobile/api
│ ├── policy/ # typed policy schemas, evaluators (attendance, leave, payroll)
│ ├── payroll-engine/ # pure, deterministic calculation library (+ golden tests)
│ └── ui/ # shared design tokens
├── infra/ # Terraform (Stage B), Dokploy compose (Stage A), k6 load tests
└── website/ # Docusaurus documentation (this site)
2.2 Key internal patterns
| Pattern | Why | Where |
|---|
Effective-dated records (valid_from, valid_to) | History-correct payroll and leave; fixes Spryple's "changes only affect new employees" | Job assignment, compensation, policy assignments, statutory tables |
| Versioned typed policies (JSON Schema + evaluator) | One self-describing definition per rule set; UI, validation and docs are generated from it | packages/policy |
| Ledgers (append-only) | Auditability; balances are derived | Leave balance, advance/loan recovery, claims payout |
| Workflow engine (approval chains as data) | One approval UX for every request type; multi-level, delegation, SLA escalation | ntf module |
| Transactional outbox → event bus → BullMQ | Reliable notifications and integrations; no lost emails | All modules |
| Deterministic recompute | An attendance day or payroll line is recomputed from its inputs; idempotent jobs | att.day-computer, pay.run |
| Immutable payroll snapshots | Reproduce exactly what was paid; legal defensibility | pay_run, pay_run_line (frozen inputs + outputs) |
3. Multi-tenancy decision (ADR-002, ADR-003)
3.1 Tenancy models compared
| Criterion | A. Shared DB, shared schema (tenant_id + RLS) | B. Shared DB, schema-per-tenant | C. Database-per-tenant (shared server) | D. Full stack per tenant (single-tenant) |
|---|
| Infra cost per tenant | Lowest | Low | Medium | Highest |
| Operational complexity | Lowest: one migration run | High: N schemas to migrate; catalogue bloat beyond ~1k schemas | High: N databases to migrate, connect and back up | Very high: N deployments |
| Isolation strength | Logical (app + RLS) | Logical (search_path) | Physical DB boundary | Complete |
| Noisy-neighbour risk | Highest (mitigate with quotas) | High | Medium | None |
| Per-tenant backup/restore | Hard (needs a logical export tool) | Medium | Easy | Easy |
| Cross-tenant analytics | Easy | Medium | Hard | Hard |
| Onboarding speed | Seconds (insert rows) | Seconds–minutes | Minutes | Hours (automated) / days |
| Fits SME pricing | Yes | Yes | Marginal | No |
| Fits enterprise isolation demands | Partially | Partially | Yes | Yes |
3.2 Recommendation: hybrid cell architecture, "pooled by default, dedicated on demand"
- Standard, Professional and Lite plans go to pooled cells (model A). Each cell is one Postgres primary + replica, a Redis instance, a set of app and worker containers, and an S3 prefix.
- The Enterprise plan goes to a dedicated cell (model C, "own DB on a shared app tier") or, when a contract demands it, model D (own app tier, optionally own AWS account or VPC). It runs the same images, the same migrations and the same release train, only pinned to a later rollout wave.
- Cells cap the blast radius. An outage, a bad migration or a noisy tenant affects one cell. New cells are created when a cell reaches its capacity thresholds (§13), not per tenant.
- Tenant mobility: a tenant can move between cells with a per-tenant export/import tool (logical copy by
tenant_id, verify, cut over through the control-plane routing switch). This is also the per-tenant backup/restore mechanism for pooled cells.
Why not schema-per-tenant? It costs nearly the same operationally as a database per tenant (N migrations) and isolates much less. Postgres catalogue growth also hurts at the scale we target.
3.3 Isolation controls in pooled cells (defence-in-depth)
- Tenant context is resolved once per request (§5), placed in AsyncLocalStorage, and never taken from the request body.
- Prisma client extension: every query runs in a transaction that first executes
SELECT set_config('app.tenant_id', $1, true) (transaction-local, equivalent to SET LOCAL, and safe with PgBouncer transaction pooling). The repository layer also adds tenant_id filters explicitly.
- PostgreSQL RLS on every tenant-owned table:
USING (tenant_id = current_setting('app.tenant_id')::uuid) with FORCE ROW LEVEL SECURITY. The app connects as a role that cannot bypass RLS. Migrations run as a separate owner role.
- Composite keys and foreign keys include
tenant_id, so a child row cannot point at another tenant's parent.
- Object storage keys are
t/{tenant_id}/{module}/{uuid}, and presigned URLs are scoped to that prefix with a short TTL.
- Cache keys, queue job payloads and search indexes are namespaced by tenant. Workers re-establish the tenant context from the job payload and verify it against the control plane.
- Automated isolation tests in CI: for every repository method, a fixture tenant B must never see tenant A rows; RLS policy coverage is checked (every table with a
tenant_id column must have a policy).
- Per-tenant rate limits and job concurrency quotas (Redis token buckets; BullMQ group concurrency) against noisy neighbours.
4. Control plane (ADR-004)
The control plane is a separate NestJS service with its own Postgres database. It is the system of record for who the tenants are and where they run.
| Capability | Details |
|---|
| Tenant registry | tenant(id, slug, legal_name, status: trial/active/suspended/churned/deleted, plan, cell_id, region, created_at) |
| Cell registry | cell(id, type: pooled/dedicated, region, db_dsn_ref, version, wave, capacity metrics) |
| Provisioning | Idempotent workflows: create the tenant → seed reference data → create the owner invite → DNS → welcome |
| Plans & entitlements | plan, entitlement(feature_key, limit), tenant_override. The app checks entitlements through a cached SDK (Lite/Standard/Pro/Enterprise and add-ons) |
| Feature flags | Kill switches and gradual rollouts per tenant, cell or plan (OpenFeature-compatible SDK; flags stored in the control plane) |
| Billing & metering | Daily usage snapshots (active employees, tracked users, SMS/WhatsApp count, storage GB), Razorpay subscription sync, invoices, dunning → suspend |
| Version management | Cell version, rollout waves, maintenance windows, per-tenant release notes |
| Support access | Time-boxed "break-glass" impersonation that requires a reason; consented by the tenant owner when the plan requires it; read-only by default; fully audited and visible to the tenant |
| Health & usage | Per-tenant KPIs: DAU, punches per day, payroll run status, error rates, last login, NPS |
| Data lifecycle | Export the tenant (DPDP portability), schedule deletion, legal hold |
The app plane never trusts the browser for the tenant. It resolves host or company code → tenant ID → cell, using a signed, cached lookup (TTL 60 s, invalidated by events).
5. Tenant addressing, domains & routing (ADR-005)
| Channel | Mechanism |
|---|
| Web | https://{slug}.spyrl.app (wildcard DNS + wildcard TLS). The slug is chosen at sign-up and is immutable, with alias redirects on rename |
| Custom domain (Enterprise) | The client adds a CNAME hr.client.com → {slug}.cname.spyrl.app; the platform verifies it with a TXT record and issues a certificate automatically (ACM / Caddy on-demand TLS) |
| Mobile | The user enters a Company Code (a human-friendly alias of the slug) once; the app calls GET /public/tenants/resolve?code= → API base URL + branding; then it stores this |
| Public API | https://api.spyrl.app/v1 with an OAuth client-credentials token bound to one tenant |
| Biometric devices | Device serial registered to a tenant and location; https://ingest.spyrl.app/devices/{serial} with a per-device secret |
| Routing to cells | The edge or app gateway maps tenant → cell. With one pooled cell (early stage) this is a no-op; later the ALB/ingress host rules or a thin router service forwards to the right cell |
6. Authentication & authorisation (ADR-006)
6.1 Authentication
- Primary: email or username + password (argon2id, breached-password check, per-tenant password policy generated from the typed policy). No default passwords: users activate through a single-use invite link (72 h expiry) sent by email, SMS or WhatsApp.
- Tokens: a 15-minute access JWT (claims:
sub, tid, sid, roles_hash) plus a rotating refresh token (httpOnly cookie on web; secure storage on mobile), with refresh-token reuse detection.
- MFA: TOTP, mandatory for
org_owner, hr_admin, finance_admin, payroll_manager and platform roles; optional for others. Recovery codes are provided.
- SSO (Standard+): OIDC with Google Workspace and Microsoft Entra ID; SAML 2.0 on Enterprise. JIT linking happens by verified email only to pre-existing employees.
- Mobile device binding: the first login registers the device. A new device needs line-manager approval when the tenant policy enables it (as in Spryple CRM device approvals). Remote sign-out is available.
- Kiosk mode: the device is enrolled with a kiosk token (location-scoped, punch-only). Employees identify themselves by face or PIN with a liveness check; the kiosk can never open other screens.
- Sessions: list and revoke sessions; idle timeout set by tenant policy.
6.2 Authorisation: RBAC + data scopes
- Permissions follow the form
{module}.{resource}.{action}. Roles are seeded templates (see CONVENTIONS.md §4) that tenants can clone and edit.
- Field-level protection for sensitive fields (salary, bank, Aadhaar, PAN, health/biometric):
pay.compensation.read, emp.sensitive.read. These fields are masked by default in lists and exports.
- Segregation of duties (configurable per tenant): the person who edits compensation cannot approve the payroll run that includes the edit.
- Every allow decision on a sensitive resource and every deny is written to the audit log.
7. Data protection & security (ADR-007)
| Control | Implementation |
|---|
| Data residency | All tenant data in AWS ap-south-1 (Mumbai). Encrypted backups copied to ap-south-2 (Hyderabad). No cross-border processing without the tenant's consent (e.g. email sending through SES in-region) |
| Encryption in transit | TLS 1.2+ everywhere; HSTS; certificate pinning in mobile is optional (it complicates rotation, so it is not used by default) |
| Encryption at rest | RDS/S3/EBS with KMS. Application-level envelope encryption for Aadhaar, PAN, bank account and face templates (per-tenant data keys wrapped by KMS) |
| Secrets | AWS Secrets Manager / SSM; nothing in images or repos; rotation every 90 days |
| Personal-data governance (DPDP Act 2023 + Rules) | Purpose-tagged fields; consent records for biometric face templates and location tracking; employee data-access requests; retention schedules per data class (e.g. raw GPS 90 days, punch photos 180 days, payroll 8 years); erasure workflows that respect statutory retention; a breach-notification runbook. Verify obligations with counsel. |
| Biometric data | Store templates/embeddings, not raw face images, wherever the provider allows. Punch selfies are kept as evidence for the retention period, then purged. Use liveness detection. Never use biometrics for any other purpose |
| Location data | Collected only between punch-in and punch-out (field users) after explicit consent; on-screen tracking indicator; aggregate after retention |
| Application security | OWASP ASVS L2 target; input validation with zod; parameterised queries (Prisma); CSP; rate limits; file-upload scanning (ClamAV) and type/size checks; SSRF-safe webhook delivery |
| Audit | An append-only audit_event table (who, tenant, action, entity, before/after hash, IP, device); tenants can export it; platform access is also logged |
| Assurance | SAST and dependency scanning in CI; container scanning; annual external pentest; ISO 27001 / SOC 2 Type I readiness by R4 if enterprise demand appears |
8. Client-specific configuration & customisation (ADR-008)
Principle: configuration, not customisation. No per-client code branches.
| Layer | Mechanism | Example |
|---|
| Plan entitlements | Control-plane plan + add-ons | FFM enabled; 200-employee limit |
| Feature flags | Per tenant, cell or plan | Early access to the new payroll UI |
| Typed policies (effective-dated) | packages/policy schemas; assigned by applicability | Attendance policy "Factory-Shift-A" for location = Plant 1 |
| Masters & reference data | Tenant CRUD | Departments, leave types, pipeline stages, expense categories |
| Custom fields | Per entity (employee, client, claim); typed; reportable | "Uniform size", "Vehicle number" |
| Templates | Letters, payslip layout, notification text (Handlebars-like, sandboxed) | The payslip shows the company logo and custom footer |
| Workflows | Approval chains as data | Leave > 3 days → line manager → HR |
| Branding | Logo, colours, login background, email sender name | — |
| Localisation | i18n bundles (English first; Hindi, Telugu and Tamil later) + tenant overrides of labels | "Comp-off" → "Compensatory leave" |
| Integrations | Per-tenant connector config | Tally export ledger names |
Anything a client wants beyond this becomes a product feature behind a flag, available to every tenant on that plan.
9. Client onboarding & provisioning
- Self-serve trial: provisioning in under 2 minutes into a pooled cell, with optional sample data.
- Setup wizard with a progress checklist (a "go-live readiness score").
- Import toolkit: versioned Excel templates with validation, dry-run, error report and idempotent commit. It covers employees, opening leave balances, YTD payroll figures, compensation and bank details.
- Dedicated tenants: the provisioning workflow creates the database, or the full stack through Terraform modules, with the same seed steps. Target time: under 1 day, fully automated.
- Offboarding: the tenant can export everything (CSV/JSON + documents zip). After a grace period: suspended → data deleted after 30/90 days per contract → deletion certificate.
10.1 Options
| Option | Infra cost (early) | Ops complexity | Scalability | Reliability | Security & isolation | Maintenance | Tenant onboarding |
|---|
| 1. Dokploy on EC2 (Docker/Compose/Swarm) + managed RDS + S3 | Low (~₹35–55k/month on-demand for prod + DR copies; lower with reserved instances; estimate) | Low (team already uses Dokploy) | Vertical + a few nodes; manual beyond ~3 nodes | Good with RDS Multi-AZ; app-tier HA needs 2+ nodes | Good (VPC, SGs); dedicated cells need extra hosts | Low–medium (OS patching) | Instant for pooled cells |
| 2. AWS ECS Fargate + RDS + ElastiCache + S3 (Terraform) | Medium (~₹60–120k/month at moderate scale, estimate) | Medium | High (autoscaling, per-cell services) | High (multi-AZ, managed) | High (IAM roles per task, per-cell isolation, separate accounts possible) | Low (no servers) | Automated dedicated cells via Terraform modules |
| 3. Kubernetes (EKS) | Medium–high (control-plane fee + nodes) | High | Very high | High | High (namespaces, network policies) | High (upgrades, add-ons) | Automated via Helm/ArgoCD |
| 4. PaaS (Render / Railway / Fly.io) | Low–medium | Very low | Medium | Medium | Medium; India residency limited | Very low | Fast |
| 5. Dedicated VM per client (classic single-tenant hosting) | Highest per client | Very high | Per client | Varies | Highest isolation | Very high (N upgrades) | Slow |
| 6. On-premise installs at the client | Client pays hardware | Extreme | — | Client-dependent | Client-controlled | Extreme (version sprawl) | Very slow |
10.2 Recommendation: staged
| Stage | When | Platform |
|---|
| Stage A — Launch | Pilot to roughly the first 50 paying tenants or 10k employees, with no contractual 99.9% SLA | Dokploy (existing team practice) on 2× EC2 (ap-south-1, different AZs) for app and workers, plus RDS PostgreSQL Multi-AZ with PITR, ElastiCache Redis, S3, CloudFront. Dev/UAT also on Dokploy. |
| Stage B — Scale | Any trigger: more than 50 tenants or 10k employees; the first Enterprise/dedicated contract; a 99.9% SLA commitment; ops toil above ~4 h/week on hosts | ECS Fargate per cell (api, worker and ingest services) via Terraform modules (cell module = services + RDS + Redis + S3 prefix + alarms). Dokploy stays for dev/preview environments. |
| Not recommended | — | EKS (overhead not justified until roughly 10+ cells or a multi-cloud requirement); on-premise (version sprawl destroys the single-release-train model); PaaS (residency and control) |
Why managed Postgres from day one, even in Stage A: payroll data loss is existential. RDS gives point-in-time recovery, Multi-AZ failover, automated minor patching and encrypted snapshots for little extra cost. The application tier can be simple; the data tier cannot.
10.3 Containerisation
- One multi-stage Dockerfile for
apps/api (distroless Node 22 runtime) with role by environment variable; separate images for control-plane, web (static, served from S3 + CloudFront) and admin.
- Images are tagged with the git SHA, signed (cosign) and scanned (Trivy) in CI. SBOMs are attached.
11. CI/CD, upgrades & migrations (ADR-010)
- Trunk-based development with short-lived branches; features are hidden behind flags until enabled per tenant.
- Database migrations follow expand/contract: (1) add new columns or tables, backward compatible; (2) deploy code that writes both and reads new; (3) backfill in batched jobs; (4) remove old fields in a later release. No destructive migration ships in the same release as the code change.
- Migrations run per cell through the control plane's rollout orchestrator. A failure halts the wave automatically.
- Mobile releases: Expo EAS builds; OTA updates (EAS Update) for JS-only fixes with a minimum supported app version enforced by the API (
426 Upgrade Required + a store link).
- API versioning:
/v1 public API with additive changes only; deprecations announced 6 months ahead. The internal web API is versioned with the SPA (same deploy).
- Rollback: application rollback means redeploying the previous image (migrations stay backward compatible). Data rollback uses PITR, only per cell, and only for catastrophic cases.
- Release cadence: weekly app releases; payroll-sensitive changes are frozen during each month's payroll window (from the 25th to the 5th). Fixes still go out as patches with extra review.
12. Backups & disaster recovery (ADR-011)
| Asset | Mechanism | RPO | RTO |
|---|
| PostgreSQL (each cell) | RDS automated backups + PITR (35 days), Multi-AZ synchronous standby; daily snapshot copied cross-region (ap-south-2) | ≤ 5 min (PITR); ≤ 24 h for a regional disaster | ≤ 1 h (AZ failure: ~2 min automatic); ≤ 8 h (regional) |
| Object storage | S3 versioning + Object Lock (compliance mode for payroll artefacts) + cross-region replication | ≤ 15 min | ≤ 4 h |
| Redis | Treated as ephemeral. Queues are rebuilt from the outbox; caches are warmed lazily | n/a | minutes |
| Control plane DB | Same as an app cell, plus a daily logical export | ≤ 5 min | ≤ 1 h |
| Per-tenant logical backup | Nightly per-tenant export for Enterprise tenants (encrypted, 30-day retention); on-demand for any tenant | 24 h | ≤ 2 h for a single-tenant restore into a staging cell |
- DR drills every quarter: restore a cell from a snapshot into an isolated account, run verification queries and record the actual RTO and RPO.
- Single-tenant "undo" (e.g. a client deletes data by mistake): restore PITR into a scratch database, extract that tenant's rows, then import them with conflict review. This needs a tested runbook.
| Signal | Tooling | Tenant-aware |
|---|
| Traces | OpenTelemetry SDK (NestJS auto-instrumentation) → Grafana Tempo (or Grafana Cloud) | tenant.id, cell.id span attributes |
| Metrics | OTel/Prometheus: RED metrics per route, queue depth and age, job duration, DB pool, RLS-denied count | Per-tenant high-cardinality metrics sampled: top-N tenants only |
| Logs | Structured JSON (pino) → Loki, with PII redaction at the logger | tenant_id field; no salary or Aadhaar values in logs |
| Errors | Sentry (web, mobile, API) with release tracking | Tenant tag |
| Uptime & synthetic | Checkly/Grafana synthetic: login, punch, leave-apply flows every 5 min per cell | — |
| Business SLOs | Payroll run success, payslip delivery, punch ingest lag, notification latency | Per tenant on the platform console |
SLOs (initial):
| SLO | Target |
|---|
| Availability (web and API) | 99.5% in Stage A; 99.9% in Stage B |
| API latency | p95 < 300 ms on reads, < 800 ms on writes |
| Punch acceptance | Within 2 s p95 |
| Payroll run | 1,000 employees calculated in under 5 minutes |
| Notification sending | Within 60 s p95 |
Error budgets gate the rollout waves (§11).
Capacity triggers for a new pooled cell: DB CPU above 60% sustained at the monthly peak; DB size above 500 GB; more than 300 tenants; more than 30k active employees; or a tenant's p95 latency regression attributable to neighbours.
14. Billing, subscriptions & usage tracking (ADR-013)
| Element | Design |
|---|
| Pricing metric | Per active employee per month (PEPM), counted as the maximum number of active employees in the billing month, with a minimum monthly fee per plan |
| Plans | Lite (attendance + simple payroll, up to 50 employees, mobile-first, like the Spryple Lite niche); Standard (Core HR, Attendance, Leave, Payroll, ESS/MSS); Professional (+ Claims, Exit/F&F, Engagement, SSO, API, advanced reports); Enterprise (+ dedicated cell option, custom domain, SAML, SLA 99.9%, audit export, sandbox tenant) |
| Add-ons | Field Force (per tracked user), Timesheets, LMS, face verification (per enrolled employee), WhatsApp notifications (metered) |
| Metering | Usage events → daily aggregates in the control plane (usage_daily(tenant, metric, value)) |
| Payments | Razorpay Subscriptions (UPI AutoPay, cards, e-mandate) for India; annual invoices for Enterprise; GST-compliant invoices |
| Lifecycle | Trial (14–30 days) → Active → Past due (grace 7 days, banner) → Suspended (read-only; payroll export still allowed) → Churned (export window) → Deleted |
| Entitlement enforcement | Soft limits with warnings (e.g. 105% of the plan's employee count) before a hard block; never block payroll processing for billing reasons during the payroll window without senior approval |
15. Centralised administration & client management
The platform admin console (apps/admin) runs on the control plane and is used only by Spyrl staff.
- Tenants: search, view the plan, usage and cell; change the plan; extend the trial; suspend or reactivate; trigger export or deletion.
- Cells: health, version, rollout waves, capacity and maintenance windows.
- Feature flags & entitlements: per tenant or cohort, with an audit trail.
- Support: break-glass access requests, session recordings of actions (audit), and a tenant-visible access log.
- Announcements: in-app banners per plan or cell (e.g. scheduled maintenance or statutory-rate updates).
- Reference data publishing: a versioned statutory pack (EPF/ESI ceilings, PT slabs by state, LWF, TDS slabs) published centrally with effective dates. Each tenant accepts or inherits it. A change log is shown to payroll managers.
- Health scores: activation (setup completion), adoption (DAU/MAU, punches), risk (failed payroll runs, tickets), used by customer success.
16. Integrations architecture
| Integration | Pattern | Release |
|---|
| Biometric devices (ZKTeco/eSSL and similar) | Push gateway (ADMS-style HTTP push from the device) → ingest → punches; fallback through a Windows/Linux sync agent polling device SDKs; CSV import as a last resort | R1 (CSV/API), R2 (gateway) |
| Face verification | Provider interface (FaceProvider.enroll/verify/liveness); first implementation: AWS Rekognition in ap-south-1 or an on-device SDK; consent-gated | R2 |
| Maps / geocoding | Provider interface; Google Maps Platform or Mappls (India pricing and coverage); snap-to-road for kms | R1 (geocode), R3 (tracking) |
| Email / SMS / WhatsApp / push | NTF channel adapters: SES; MSG91/Gupshup; Meta WhatsApp Cloud API through a BSP; Expo push | R1 |
| Statutory portals | File generation (EPFO ECR, ESIC monthly contribution, PT and TDS return data); no direct portal automation (legal and fragility risk) | R1 (ECR/ESI/PT), R2 (24Q/Form 16) |
| Banks | Bank file adapters (generic NEFT/RTGS CSV plus bank-specific formats); later H2H APIs | R1 |
| Accounting | Payroll journal export: Tally XML / CSV in R2; Zoho Books and ERP APIs in R4 | R2 / R4 |
| SSO | OIDC (Google, Microsoft); SAML (Enterprise) | R1 (OIDC), R2 (SAML) |
| Public API & webhooks | REST /v1 (OpenAPI 3.1), OAuth2 client credentials, signed webhooks (HMAC SHA-256 with retries and a DLQ) | R2 |
| Calendar | Leave → Google/Microsoft calendar events (optional) | R3 |
17. Scaling path as the number of clients grows
| Scale | Tenants / employees | Architecture state |
|---|
| Launch | 1–50 / ≤ 10k | One pooled cell on Stage A; a single Postgres (Multi-AZ); 2 app nodes; workers co-located |
| Growth | 50–300 / 10k–50k | Stage B (ECS). Separate API and worker services with autoscaling on queue depth. A read replica for reports. First dedicated cells |
| Scale | 300–2,000 / 50k–300k | Multiple pooled cells (P1…Pn). ingest extracted, with GPS in partitioned tables or TimescaleDB. Report workloads moved to a replica or a columnar warehouse (e.g. nightly CDC to ClickHouse/Redshift) for analytics |
| Large | 2,000+ | Cell router at the edge; per-cell rollout automation; consider multi-region only for non-India expansion |
Scaling actions are all adding cells or splitting workloads, never re-architecting tenancy. That is the main benefit of choosing cells now.
18. Architecture decision log (summary)
| ADR | Decision | Status |
|---|
| ADR-001 | Modular monolith (NestJS 11, TypeScript), process roles api/worker/ingest | Accepted |
| ADR-002 | PostgreSQL 16+, Prisma, shared schema with tenant_id + forced RLS | Accepted |
| ADR-003 | Hybrid cells: pooled by default, dedicated on demand, one release train | Accepted |
| ADR-004 | Separate control plane (registry, provisioning, entitlements, billing, flags, rollout, support access) | Accepted |
| ADR-005 | Subdomain per tenant + company code for mobile + custom domains on Enterprise | Accepted |
| ADR-006 | Built-in auth (argon2id, JWT + rotating refresh, MFA, invite activation), OIDC/SAML SSO, RBAC with data scopes | Accepted |
| ADR-007 | India residency, envelope encryption for sensitive fields, consent-gated biometrics and location | Accepted |
| ADR-008 | Configuration over customisation: typed, effective-dated policies; custom fields; templates; workflow-as-data | Accepted |
| ADR-009 | Stage A Dokploy + managed RDS → Stage B ECS Fargate + Terraform; no EKS or on-prem | Accepted |
| ADR-010 | Trunk-based, expand/contract migrations, wave rollouts, payroll-window change freeze | Accepted |
| ADR-011 | RDS PITR + cross-region snapshot copies; quarterly DR drills; per-tenant logical export | Accepted |
| ADR-012 | OpenTelemetry + Grafana stack + Sentry; tenant-tagged telemetry; SLO-gated rollouts | Accepted |
| ADR-013 | PEPM pricing, plan tiers + add-ons, Razorpay, control-plane metering | Proposed (needs commercial validation) |
| ADR-014 | Single Expo mobile app with role/entitlement navigation and kiosk mode | Accepted |
19. Implementation roadmap (architecture track)
| Milestone | Deliverables | Aligned release |
|---|
| A1 — Skeleton (weeks 1–4) | Monorepo, CI, Docker images, Dokploy dev/UAT, RDS, tenant context + RLS + isolation test harness, auth (invite, JWT, MFA), audit log, OTel + Sentry | R0 |
| A2 — Control plane MVP (weeks 3–8) | Tenant registry, self-serve provisioning, plans/entitlements SDK, feature flags, platform admin console v1 | R0 |
| A3 — Production Stage A (weeks 8–12) | Prod cell P1 on Dokploy + RDS Multi-AZ, backups and cross-region copies, synthetic checks, on-call runbooks, first DR drill | R0/R1 |
| A4 — Payroll-grade ops (by MVP GA) | Payroll-window freeze, immutable runs, per-tenant export, statutory pack publishing | R1 |
| A5 — Stage B (trigger-based, ~month 9–12) | Terraform cell module, ECS migration of P1 (blue/green), first dedicated cell, wave rollout orchestrator | R2 |
| A6 — Scale features | Ingest extraction, GPS partitioning, read replicas for reporting, public API + webhooks | R3 |
| A7 — Assurance | External pentest, ISO 27001 readiness, SAML, audit exports | R2–R4 |
Pros and cons of the recommended architecture
Pros
- The cheapest per-tenant cost in pooled cells.
- Enterprise isolation without forking the code.
- One codebase and one release train.
- Isolation enforced in three places: app, RLS and keys.
- The team's existing tools (NestJS, Prisma, Dokploy, Expo) carry over.
- A clear, trigger-based scaling path.
- India data residency.
Cons
- RLS + Prisma needs careful engineering (transaction-scoped settings and connection pooling with PgBouncer in transaction mode). Mitigation: the A1 spike and isolation tests.
- A pooled restore of a single tenant is more complex. Mitigation: the logical export tooling.
- Two stages of hosting mean one planned migration (Stage A → B). Mitigation: containers and Terraform modules from the start, so the migration is a cutover rather than a rewrite.
- A modular monolith needs boundary discipline. Mitigation: lint rules, ownership and ADR reviews.
Synced from /architecture.md in the repository root. Edit the root file, not this page.