Skip to main content

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​

DriverSourceConsequence
Many SME tenants (10–1,000 employees each), price-sensitiveBO-01, BO-04 in brd.mdPooled infrastructure; low cost per tenant; self-serve onboarding
A few large or regulated clients demanding isolationBR-PLT-05A dedicated option must exist without forking the code
Payroll and salary data, Aadhaar, PAN, bank, biometric and location dataNFR-SEC, NFR-PRVStrong tenant isolation, encryption, audit, India data residency, consent
Month-end spikes (payroll runs, payslip emails, attendance computation)NFR-PERFAsync job queue, horizontal workers, per-tenant fairness
Continuous GPS ingest from field users (R3)FFMA write-heavy path separable from the OLTP core
Statutory rules change by date and statePAYEffective-dated reference data; reproducible payroll runs
One release train, small teamBO-05Modular monolith, a single codebase, automated rollout

2. System overview​

2.1 Application plane: a modular monolith (ADR-001)​

OptionProsCons
Microservices from day oneIndependent scaling and deploysLarge 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 possibleIt scales as a unit, so boundaries need discipline
Serverless functionsPay-per-useCold 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​

PatternWhyWhere
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 itpackages/policy
Ledgers (append-only)Auditability; balances are derivedLeave balance, advance/loan recovery, claims payout
Workflow engine (approval chains as data)One approval UX for every request type; multi-level, delegation, SLA escalationntf module
Transactional outbox → event bus → BullMQReliable notifications and integrations; no lost emailsAll modules
Deterministic recomputeAn attendance day or payroll line is recomputed from its inputs; idempotent jobsatt.day-computer, pay.run
Immutable payroll snapshotsReproduce exactly what was paid; legal defensibilitypay_run, pay_run_line (frozen inputs + outputs)

3. Multi-tenancy decision (ADR-002, ADR-003)​

3.1 Tenancy models compared​

CriterionA. Shared DB, shared schema (tenant_id + RLS)B. Shared DB, schema-per-tenantC. Database-per-tenant (shared server)D. Full stack per tenant (single-tenant)
Infra cost per tenantLowestLowMediumHighest
Operational complexityLowest: one migration runHigh: N schemas to migrate; catalogue bloat beyond ~1k schemasHigh: N databases to migrate, connect and back upVery high: N deployments
Isolation strengthLogical (app + RLS)Logical (search_path)Physical DB boundaryComplete
Noisy-neighbour riskHighest (mitigate with quotas)HighMediumNone
Per-tenant backup/restoreHard (needs a logical export tool)MediumEasyEasy
Cross-tenant analyticsEasyMediumHardHard
Onboarding speedSeconds (insert rows)Seconds–minutesMinutesHours (automated) / days
Fits SME pricingYesYesMarginalNo
Fits enterprise isolation demandsPartiallyPartiallyYesYes

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)​

  1. Tenant context is resolved once per request (§5), placed in AsyncLocalStorage, and never taken from the request body.
  2. 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.
  3. 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.
  4. Composite keys and foreign keys include tenant_id, so a child row cannot point at another tenant's parent.
  5. Object storage keys are t/{tenant_id}/{module}/{uuid}, and presigned URLs are scoped to that prefix with a short TTL.
  6. 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.
  7. 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).
  8. 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.

CapabilityDetails
Tenant registrytenant(id, slug, legal_name, status: trial/active/suspended/churned/deleted, plan, cell_id, region, created_at)
Cell registrycell(id, type: pooled/dedicated, region, db_dsn_ref, version, wave, capacity metrics)
ProvisioningIdempotent workflows: create the tenant → seed reference data → create the owner invite → DNS → welcome
Plans & entitlementsplan, entitlement(feature_key, limit), tenant_override. The app checks entitlements through a cached SDK (Lite/Standard/Pro/Enterprise and add-ons)
Feature flagsKill switches and gradual rollouts per tenant, cell or plan (OpenFeature-compatible SDK; flags stored in the control plane)
Billing & meteringDaily usage snapshots (active employees, tracked users, SMS/WhatsApp count, storage GB), Razorpay subscription sync, invoices, dunning → suspend
Version managementCell version, rollout waves, maintenance windows, per-tenant release notes
Support accessTime-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 & usagePer-tenant KPIs: DAU, punches per day, payroll run status, error rates, last login, NPS
Data lifecycleExport 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)​

ChannelMechanism
Webhttps://{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)
MobileThe 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 APIhttps://api.spyrl.app/v1 with an OAuth client-credentials token bound to one tenant
Biometric devicesDevice serial registered to a tenant and location; https://ingest.spyrl.app/devices/{serial} with a per-device secret
Routing to cellsThe 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)​

ControlImplementation
Data residencyAll 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 transitTLS 1.2+ everywhere; HSTS; certificate pinning in mobile is optional (it complicates rotation, so it is not used by default)
Encryption at restRDS/S3/EBS with KMS. Application-level envelope encryption for Aadhaar, PAN, bank account and face templates (per-tenant data keys wrapped by KMS)
SecretsAWS 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 dataStore 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 dataCollected only between punch-in and punch-out (field users) after explicit consent; on-screen tracking indicator; aggregate after retention
Application securityOWASP 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
AuditAn append-only audit_event table (who, tenant, action, entity, before/after hash, IP, device); tenants can export it; platform access is also logged
AssuranceSAST 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.

LayerMechanismExample
Plan entitlementsControl-plane plan + add-onsFFM enabled; 200-employee limit
Feature flagsPer tenant, cell or planEarly access to the new payroll UI
Typed policies (effective-dated)packages/policy schemas; assigned by applicabilityAttendance policy "Factory-Shift-A" for location = Plant 1
Masters & reference dataTenant CRUDDepartments, leave types, pipeline stages, expense categories
Custom fieldsPer entity (employee, client, claim); typed; reportable"Uniform size", "Vehicle number"
TemplatesLetters, payslip layout, notification text (Handlebars-like, sandboxed)The payslip shows the company logo and custom footer
WorkflowsApproval chains as dataLeave > 3 days → line manager → HR
BrandingLogo, colours, login background, email sender name—
Localisationi18n bundles (English first; Hindi, Telugu and Tamil later) + tenant overrides of labels"Comp-off" → "Compensatory leave"
IntegrationsPer-tenant connector configTally 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. Hosting & deployment platform evaluation (ADR-009)​

10.1 Options​

OptionInfra cost (early)Ops complexityScalabilityReliabilitySecurity & isolationMaintenanceTenant onboarding
1. Dokploy on EC2 (Docker/Compose/Swarm) + managed RDS + S3Low (~₹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 nodesGood with RDS Multi-AZ; app-tier HA needs 2+ nodesGood (VPC, SGs); dedicated cells need extra hostsLow–medium (OS patching)Instant for pooled cells
2. AWS ECS Fargate + RDS + ElastiCache + S3 (Terraform)Medium (~₹60–120k/month at moderate scale, estimate)MediumHigh (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)HighVery highHighHigh (namespaces, network policies)High (upgrades, add-ons)Automated via Helm/ArgoCD
4. PaaS (Render / Railway / Fly.io)Low–mediumVery lowMediumMediumMedium; India residency limitedVery lowFast
5. Dedicated VM per client (classic single-tenant hosting)Highest per clientVery highPer clientVariesHighest isolationVery high (N upgrades)Slow
6. On-premise installs at the clientClient pays hardwareExtreme—Client-dependentClient-controlledExtreme (version sprawl)Very slow

10.2 Recommendation: staged​

StageWhenPlatform
Stage A — LaunchPilot to roughly the first 50 paying tenants or 10k employees, with no contractual 99.9% SLADokploy (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 — ScaleAny 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 hostsECS 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)​

AssetMechanismRPORTO
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 storageS3 versioning + Object Lock (compliance mode for payroll artefacts) + cross-region replication≤ 15 min≤ 4 h
RedisTreated as ephemeral. Queues are rebuilt from the outbox; caches are warmed lazilyn/aminutes
Control plane DBSame as an app cell, plus a daily logical export≤ 5 min≤ 1 h
Per-tenant logical backupNightly per-tenant export for Enterprise tenants (encrypted, 30-day retention); on-demand for any tenant24 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.

13. Monitoring, logging & performance management (ADR-012)​

SignalToolingTenant-aware
TracesOpenTelemetry SDK (NestJS auto-instrumentation) → Grafana Tempo (or Grafana Cloud)tenant.id, cell.id span attributes
MetricsOTel/Prometheus: RED metrics per route, queue depth and age, job duration, DB pool, RLS-denied countPer-tenant high-cardinality metrics sampled: top-N tenants only
LogsStructured JSON (pino) → Loki, with PII redaction at the loggertenant_id field; no salary or Aadhaar values in logs
ErrorsSentry (web, mobile, API) with release trackingTenant tag
Uptime & syntheticCheckly/Grafana synthetic: login, punch, leave-apply flows every 5 min per cell—
Business SLOsPayroll run success, payslip delivery, punch ingest lag, notification latencyPer tenant on the platform console

SLOs (initial):

SLOTarget
Availability (web and API)99.5% in Stage A; 99.9% in Stage B
API latencyp95 < 300 ms on reads, < 800 ms on writes
Punch acceptanceWithin 2 s p95
Payroll run1,000 employees calculated in under 5 minutes
Notification sendingWithin 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)​

ElementDesign
Pricing metricPer active employee per month (PEPM), counted as the maximum number of active employees in the billing month, with a minimum monthly fee per plan
PlansLite (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-onsField Force (per tracked user), Timesheets, LMS, face verification (per enrolled employee), WhatsApp notifications (metered)
MeteringUsage events → daily aggregates in the control plane (usage_daily(tenant, metric, value))
PaymentsRazorpay Subscriptions (UPI AutoPay, cards, e-mandate) for India; annual invoices for Enterprise; GST-compliant invoices
LifecycleTrial (14–30 days) → Active → Past due (grace 7 days, banner) → Suspended (read-only; payroll export still allowed) → Churned (export window) → Deleted
Entitlement enforcementSoft 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​

IntegrationPatternRelease
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 resortR1 (CSV/API), R2 (gateway)
Face verificationProvider interface (FaceProvider.enroll/verify/liveness); first implementation: AWS Rekognition in ap-south-1 or an on-device SDK; consent-gatedR2
Maps / geocodingProvider interface; Google Maps Platform or Mappls (India pricing and coverage); snap-to-road for kmsR1 (geocode), R3 (tracking)
Email / SMS / WhatsApp / pushNTF channel adapters: SES; MSG91/Gupshup; Meta WhatsApp Cloud API through a BSP; Expo pushR1
Statutory portalsFile 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)
BanksBank file adapters (generic NEFT/RTGS CSV plus bank-specific formats); later H2H APIsR1
AccountingPayroll journal export: Tally XML / CSV in R2; Zoho Books and ERP APIs in R4R2 / R4
SSOOIDC (Google, Microsoft); SAML (Enterprise)R1 (OIDC), R2 (SAML)
Public API & webhooksREST /v1 (OpenAPI 3.1), OAuth2 client credentials, signed webhooks (HMAC SHA-256 with retries and a DLQ)R2
CalendarLeave → Google/Microsoft calendar events (optional)R3

17. Scaling path as the number of clients grows​

ScaleTenants / employeesArchitecture state
Launch1–50 / ≤ 10kOne pooled cell on Stage A; a single Postgres (Multi-AZ); 2 app nodes; workers co-located
Growth50–300 / 10k–50kStage B (ECS). Separate API and worker services with autoscaling on queue depth. A read replica for reports. First dedicated cells
Scale300–2,000 / 50k–300kMultiple 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
Large2,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)​

ADRDecisionStatus
ADR-001Modular monolith (NestJS 11, TypeScript), process roles api/worker/ingestAccepted
ADR-002PostgreSQL 16+, Prisma, shared schema with tenant_id + forced RLSAccepted
ADR-003Hybrid cells: pooled by default, dedicated on demand, one release trainAccepted
ADR-004Separate control plane (registry, provisioning, entitlements, billing, flags, rollout, support access)Accepted
ADR-005Subdomain per tenant + company code for mobile + custom domains on EnterpriseAccepted
ADR-006Built-in auth (argon2id, JWT + rotating refresh, MFA, invite activation), OIDC/SAML SSO, RBAC with data scopesAccepted
ADR-007India residency, envelope encryption for sensitive fields, consent-gated biometrics and locationAccepted
ADR-008Configuration over customisation: typed, effective-dated policies; custom fields; templates; workflow-as-dataAccepted
ADR-009Stage A Dokploy + managed RDS → Stage B ECS Fargate + Terraform; no EKS or on-premAccepted
ADR-010Trunk-based, expand/contract migrations, wave rollouts, payroll-window change freezeAccepted
ADR-011RDS PITR + cross-region snapshot copies; quarterly DR drills; per-tenant logical exportAccepted
ADR-012OpenTelemetry + Grafana stack + Sentry; tenant-tagged telemetry; SLO-gated rolloutsAccepted
ADR-013PEPM pricing, plan tiers + add-ons, Razorpay, control-plane meteringProposed (needs commercial validation)
ADR-014Single Expo mobile app with role/entitlement navigation and kiosk modeAccepted

19. Implementation roadmap (architecture track)​

MilestoneDeliverablesAligned 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 + SentryR0
A2 — Control plane MVP (weeks 3–8)Tenant registry, self-serve provisioning, plans/entitlements SDK, feature flags, platform admin console v1R0
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 drillR0/R1
A4 — Payroll-grade ops (by MVP GA)Payroll-window freeze, immutable runs, per-tenant export, statutory pack publishingR1
A5 — Stage B (trigger-based, ~month 9–12)Terraform cell module, ECS migration of P1 (blue/green), first dedicated cell, wave rollout orchestratorR2
A6 — Scale featuresIngest extraction, GPS partitioning, read replicas for reporting, public API + webhooksR3
A7 — AssuranceExternal pentest, ISO 27001 readiness, SAML, audit exportsR2–R4

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.