🏫 School Platform — Tenancy, Security, Backup & DR
The production design for serving many schools from one domain with
per-school isolation (your option E, upgradeable to F), a NestJS backend,
an hourly→daily backup lifecycle, and a tested disaster-recovery plan.
ArchitectureOption E now (per-school silos + control plane) — F's aggregation gate added when cross-school analytics is needed
Tenancy routingOne domain, no subdomains — the external auth token proves identity only; the active school travels as an X-School-Id header, membership-checked on every request
BackendNestJS: guards for auth & roles, middleware for tenant resolution, one connection pool per school
1 Architecture: E today, F when analytics needs it
Between your shortlisted options E and F: they are not rivals — F is E plus one
extra component (a governed aggregation gate). So the decision is about sequencing, not choosing.
Start with E — per-school silos + control plane
One database per school — own schema, own credentials, own backup line. A connection to school A physically cannot read school B.Isolation is structural, not a WHERE clause.
Control plane — auth, school registry (catalog), routing, feature flags. Small, boring, its own database.
One shared app fleet — the same NestJS pods serve every school; the verified identity plus the validated X-School-Id header decide which school database the request touches.
Add F's aggregation gate later
Trigger to build it: the first real need for cross-school insight (benchmarks, platform dashboards, per-feature usage).
Aggregates only, one way — a scheduled job computes school-level aggregates (counts, averages) into a separate analytics store. No row-level data ever leaves a silo.
Governance built in: minimum cohort size (e.g. suppress any aggregate covering < 5 schools), audit log of every aggregate query, no ad-hoc row egress path.
Decision: ship E; keep every silo's schema identical (same migrations everywhere) so the F gate is a pure add-on, not a refactor.
2 One domain, no subdomains — how UI → API knows the school
Everything lives on app.yourschools.com. The auth service is
external — we don't control its token claims — so its token proves only who the
user is. Which school is active travels as an X-School-Id header, and the API
validates that header against the membership table on every single request. The header is
a hint; our control plane is the authority.
1Login (external)Redirect to the auth service's hosted login. It returns its JWT — identity only: sub, email.
2Token verificationAPI verifies via the service's JWKS: signature, issuer, audience, expiry. No custom claims needed.
3Membership lookupControl plane maps sub → schools + role in each. OUR data — the token knows nothing about schools.
4School picker1 school → auto-selected. 2+ → picker screen (see §3). Choice becomes the X-School-Id header.
5Every API callUI sends token + X-School-Id. TenantGuard re-checks membership (cached ~60 s) — non-members get 403.
6Silo connectionCatalog maps school_id → DB host + secret; a cached pool for that school serves the query.
Three sources, three jobs
// 1. From the AUTH SERVICE token (we don't control its claims):
{ "sub": "auth0|8123", "email": "priya@dps.edu", "exp": ... } // WHO
// 2. From OUR control plane (queried per request, cached 60 s):
memberships: auth0|8123 → [ { school: "sch_dps_pune", role: "school_admin" },
{ school: "sch_svm_mumbai", role: "teacher" } ] // MAY + AS WHAT
// 3. From the CLIENT (untrusted until checked):
X-School-Id: sch_dps_pune // WHICH — TenantGuard validates it EVERY request
Iron rules
The X-School-Id header is untrusted input — TenantGuard checks (sub, school) against the membership table on every request; a non-member gets 403 before any handler runs. Trusting the header without this check would be the cross-tenant bug.
Roles come from our membership table, never from the token — the auth service only proves identity; authorization is entirely ours.
Bonus over JWT claims: revoking someone's access = delete the membership row — effective within the 60 s cache, no waiting for a token to expire.
Every DB access goes through the tenant-scoped connection — there is no "global" pool that can see all schools from request code.
3 Users who belong to two schools
A teacher can work at two schools; a director can own five. The rule:
one active school per session, chosen explicitly before any dashboard loads.
The flow
After login, if memberships.length > 1 the UI routes to /choose-school — a card per school (name, logo, the user's role there).
Selection sets the active school: saved as last-choice via POST /me/active-school, and from then on every request carries X-School-Id with that value — which the server membership-checks each time.
Only then does the app fetch dashboard data. There is no "no school selected" state after this point.
A school switcher stays in the top-nav; switching changes the header value and fully reloads dashboard state (no data from school A ever remains mounted while school B is active). A wrong or stale header value can't leak anything — the per-request check 403s it.
Details that prevent pain
Remember the last choice per user (control plane), preselect it — but still show the picker on first login of the day for multi-school users.Fast for the 95% case, explicit for auditability.
Roles are per membership — the same person can be school_admin at school A and plain teacher at school B. The token carries the role of the active school only.
Deep links: a link opened while the "wrong" school is active shows a one-click "Switch to DPS Pune to view this" interstitial — never silently switches.
4 NestJS blueprint
NestJS fits this design well: guards for authn/authz, middleware for tenant
resolution, and dependency injection for the per-school connection. The one hard problem is
pool-per-school — solved with a connection manager, LRU-capped.
// tenant.guard.ts
const { sub } = await verifyIdpJwt(req); // JWKS of the external auth service
const schoolId = req.headers['x-school-id'];
const member = await this.memberships
.check(sub, schoolId); // control plane, cached 60 s
if (!member) throw new ForbiddenException();
req.tenant = { schoolId, role: member.role,
conn: await this.connections.forSchool(schoolId) };
// connection.manager.ts
async forSchool(id: string) {
if (this.pools.has(id)) return this.pools.get(id);
const cfg = await this.catalog.dbConfig(id);
// ↑ host + creds from Secrets Manager, cached 5 min
const pool = createPool({ ...cfg, max: 5 });
this.pools.set(id, pool); // LRU-evict past ~200
return pool;
}
Small pools × many schools — 5 conns/school; PgBouncer in front once schools × pools approaches Postgres limits.
Migrations for all silos — one command loops the catalog and applies migrations per school; a new school gets them at onboarding.
5 API security
AuthN: delegated to the external auth service — we verify its JWT via JWKS (signature, issuer, audience, expiry) and cache the JWKS keys. Short-lived tokens + its refresh flow are the auth service's job.
AuthZ (entirely ours): two layers — role guard (@Roles('school_admin'), role read from our membership table) and tenant guard (validated X-School-Id membership check). Both run on every route.
Validation: global ValidationPipe with whitelist: true, forbidNonWhitelisted: true — unknown fields are rejected, not ignored.
Rate limiting:@nestjs/throttler — tight on /auth/* (brute-force), generous per-user elsewhere; plus AWS WAF on the ALB (SQLi/XSS rulesets, IP throttles).
Headers & CORS: helmet defaults; CORS locked to the one domain — a nice side-benefit of the no-subdomain decision.
Secrets: nothing in env files or images — pulled from AWS Secrets Manager at boot (and per-school DB creds at connect time).
Audit log: control-plane table: who, school, action, entity, timestamp, IP — written for every mutation and every auth event, immutable (no UPDATE/DELETE grants).
Dependencies: ECR scan-on-push (already on) + npm audit/pip-audit jobs in CI.
6 Database security
Network: databases in private subnets only; security group admits port 5432 from the EKS node/pod SG only. No public endpoint, ever.
Encryption: at rest with KMS (per-instance keys; premium schools can get their own CMK), in transit with enforced TLS (rejectUnauthorized on the client).
Per-school credentials: each silo has its own DB user that can access only its own database — no superuser in the app path. Secrets Manager rotation every 30–90 days.
Least-privilege grants: app user gets DML only (SELECT/INSERT/UPDATE/DELETE); DDL runs only through the migration job's separate role.
Human access: no shared psql passwords — SSM Session Manager + IAM auth for break-glass, every session logged.
Audit & detection: pgAudit on DDL + role changes; CloudTrail on RDS API calls; alarms on failed-login spikes and unusual egress volume.
Isolation as the last line: even if a bug ships, a school-A connection cannot express a query that touches school B — the blast radius of any single mistake is one school.
7 Backup policy — your retention rules, implemented
Requirement: hourly backups always available for recent history; after 10 days
keep only dailies and drop the hourlies. Implemented with per-school dumps to S3 + lifecycle
rules, plus RDS point-in-time recovery underneath.
Tier
Taken
Kept
Mechanism
Restores
PITR
continuous (WAL)
10 days
RDS automated backups
any second in the last 10 days — beats even hourly granularity
Hourly
every hour, per school
10 days, then auto-deleted
k8s CronJob: pg_dump → s3://backups/hourly/<school>/ · S3 lifecycle expires at 10 d
Bucket: versioned, encrypted (KMS), Object Lock optional for tamper-proofing, replicated to the DR region (§8).
Non-negotiables
Per-school files — restoring school #42 never touches the other silos (the whole point of option E).
Backup monitoring: the CronJob writes a success marker per school; a daily check alerts on any school with no fresh backup — silent backup failure is the classic DR killer.
Restore drills: monthly, restore one random school's dump into a scratch instance and run a row-count sanity check. A backup that was never restored is a hope, not a backup.
8 Disaster-recovery plan
Targets: RPO ≤ 1 hour (per your backup rule; effectively minutes via PITR)
and RTO tiered by scenario. Everything is restorable from code + backups because the
entire infrastructure is Terraform.
Scenario
Who fixes it
RTO
How
Pod / node crash
automatic
seconds–minutes
Kubernetes self-healing + HPA; multi-AZ node group
kubectl rollout undo — previous image tag is one command away (SHA-tagged images)
Data corruption / bad migration (one school)
runbook
< 1 h
PITR to just-before, or restore that school's hourly dump into a fresh DB, repoint its catalog entry
Ransom/tamper of primary data
runbook
hours
restore from versioned, cross-region-replicated S3 (Object Lock protects history)
Full region outage
runbook
< 4 h
see the region runbook ↓
Region-loss runbook (drilled twice a year)
1. Declare — one person is incident commander; status page updated first.
2. Provision — terraform apply against the DR region (same code, different aws_region var). ~20 min for VPC + EKS + ECR.
3. Images — ECR cross-region replication means images are already there.
4. Data — restore each school from the replicated S3 backups (parallel jobs, catalog-driven; priority order: largest schools first).
5. Cutover — Route 53 flips the one domain to the new ALB (low TTL kept at 60 s). Single domain = single DNS change.
6. Verify — smoke suite per school (login, dashboard, one write), then close the incident with a written timeline.
Why this works: the three replicated things — Terraform code (git), images (ECR replication), data (S3 cross-region) — are the entire system. Everything else is derived.
9 Per-school admin panel & feature control
Two admin surfaces, strictly separated: a school admin panel for each school's
own staff, and a platform panel for you. Feature flags live in the control plane so features
can be turned on per school without deploys.
Roles (per membership)
Role
Scope
Can
platform_admin
you / your team
onboard schools, set plans & feature flags, view aggregates (F-gate only), never reads school rows in normal operation
school_admin
one school
manage that school's users & roles, branding, toggle features within their plan, view school audit log
6. Admin panels: school admin first (users + feature toggles), then the platform panel (onboarding = the automation from the multi-tenancy diagram).
7. First DR drill — restore one school + region-runbook tabletop. Only after this call the platform production-ready.
8. Later: F's aggregation gate when cross-school analytics is requested.
11 Architecture diagram, DevOps & cost
The whole system in one picture, the pipeline that ships it, and what it costs
per month at three sizes — pilot, production and scale.
Architecture at a glanceOne request path: user → Route 53 → WAF/ALB →
NestJS pods → membership check against the control plane → that school's silo (§2). Identity
comes from the external auth service, verified via JWKS. CronJobs read the silos and write
per-school dumps to S3 (§7). In a region loss, Route 53 flips the same domain to the DR
region's ALB (§8).
Compute options compared — why EKS
Option
Ops load
Pool-per-school (§4)?
Pilot base / mo
Verdict
EKS — managed Kubernetes
medium–high
✔ long-lived pods hold the pools
~$175
chosen team, repo, Terraform and the §8 DR runbook are already k8s-shaped; portable off AWS
ECS Fargate — AWS containers, no nodes to manage
low
✔ tasks are long-lived
~$110
closest rival the greenfield pick for a team without k8s skills; no $73 control-plane fee, but AWS-only and our infra code would need a rewrite
cheapest at pilot wins on spiky traffic — ours is steady school-hours load, where always-on is cheaper; still right for cron-shaped work later (EventBridge)
App Runner — container PaaS
very low
~ long-lived, but scale-to-zero reintroduces cold starts
~$60
too cramped clunky VPC model, no cron jobs — we'd bolt Lambda on anyway; fine for one small service, not a platform
Elastic Beanstalk — legacy PaaS on EC2
medium
✔
~$90
skip maintenance-mode energy, awkward with Terraform; nothing it does that ECS/EKS don't do better
Plain EC2 — VMs + Docker Compose
high (patching, SSH deploys)
✔
~$60
demo only no self-healing, no rolling deploys; the §8 runbook becomes slow and manual
skip PaaS options conflict with the VPC/KMS/WAF design; Nomad's ecosystem is too small to bet on
Rule of thumb: greenfield on AWS with no Kubernetes experience →
ECS Fargate; existing k8s investment or multi-cloud ambitions → EKS. We are squarely the second
case. The decision is per-workload, not all-or-nothing: the API fleet stays on EKS, and the
cron-shaped work (backups, freshness checks) can move to EventBridge + Lambda later without
touching the core design.
DevOps approach
Everything is code
Terraform owns all infrastructure — VPC, EKS, RDS, S3 lifecycle, WAF, Route 53. State in S3 with DynamoDB locking; the AWS console is read-only.
Kubernetes manifests (Helm) live in the same repo — reviewed and versioned like any code.
School onboarding is a pipeline job, not a checklist: create DB + user + secret, run migrations, insert the catalog row.
Same modules for production, sandbox and the DR region — only tfvars differ. This is what makes the §8 runbook a variable change, not a rebuild-from-memory.
Environments
Two cloud environments only. Local dev is docker-compose on the laptop — free, and not a deployed environment.
sandbox — prod-shaped but scaled down: single-AZ RDS, spot nodes, two demo schools. Auto-deployed on every merge; also where restore drills and demos run.
production — multi-AZ everything; changes arrive only through the pipeline's manual gate.
DR — no standing environment: code (git) + replicated images (ECR) + replicated backups (S3) are the whole system, so standing cost is near zero.
1Push / PRlint, unit tests, npm audit run in CI — red blocks the merge.
2BuildDocker image tagged with the git SHA — never latest; rollback needs exact tags.
3PublishPush to ECR; scan-on-push blocks critical CVEs.
4SandboxAuto-deploy; migrations loop the catalog per school; smoke suite runs.
5ApproveOne-click manual gate — the only human step between merge and prod.
Structured JSON logs with school_id + request id on every line → CloudWatch Logs; one query answers "what happened to school X at 10:04".
Metrics: p95 latency, 5xx rate, pod restarts, HPA saturation; per-silo RDS CPU / storage / connections.
Alerts: 5xx spike, p95 breach, backup-freshness per school (§7), RDS storage > 80%, certificate expiry, failed-login spikes.
Dashboards: one platform overview + a per-school drill-down keyed by school_id.
What it costs (monthly, ap-south-1)
Component
Pilot — ~5 schools
Production — ~50 schools
Scale — ~200 schools
EKS control plane
$73
$73
$73
Worker nodes (EC2)
2 × t3.medium — $67
3 × t3.large — $190
6 × m6g.large — $420
RDS Postgres
db.t4g.micro, 1-AZ — $15
db.t4g.medium multi-AZ + 100 GB — $145
2 × r6g.large multi-AZ + 500 GB — $850
ALB + WAF
$25
$45
$80
NAT gateway
$36
$40
$70
S3 backups (+ Glacier)
$3
$15
$50
Secrets Manager
$3
$25
$85
CloudWatch · Route 53 · ECR
$10
$27
$60
Sandbox environment
—
$80
$120
Total ≈
$230 / mo (~₹19k)
$640 / mo (~₹54k)
$1,810 / mo (~₹1.5L)
Per school ≈
$46
$13
$9
Cost levers (biggest first)
Database-per-school ≠ instance-per-school — many silo databases share one RDS instance while keeping option E's isolation (separate DB + separate credentials). Promote heavy schools to their own instance only when they earn it.
Savings Plans / reserved instances once load is steady — 30–40% off EC2 + RDS.
Graviton (t4g / m6g / r6g) — already assumed in the table, ~20% cheaper than x86.
The §7 lifecycle rules are a cost control too — hourlies die at 10 days, Glacier after 60.
Cost guardrails
AWS Budgets — alerts at 80% and 100% of the monthly target; anomaly detection on.
Cost-allocation tags on everything (env, school_id where possible) — Cost Explorer then shows cost per school.
Weekly cost review is part of ops, like uptime — the per-school number is the one to watch: it should keep falling as schools grow.
Estimates, not quotes: on-demand ap-south-1 prices as of Aug 2026 (~₹84/$),
excluding data transfer, domain and support plan. Re-price in the AWS Pricing Calculator before
committing.
12 Compute options compared — why EKS
Every AWS way to run the NestJS fleet, judged against this design:
the pool-per-school connection manager (§4) needs long-lived processes, the security posture (§6)
needs real VPC/KMS integration, and the DR runbook (§8) assumes declarative redeploys.
Costs are rough pilot-scale on-demand figures per month.
Option
Ops load
Pool-per-school (§4)?
Pilot / mo
Verdict
EKS — managed Kubernetes
medium–high
✔ long-lived pods hold the pools
~$175
chosen team, repo, Terraform and the §8 DR runbook are already k8s-shaped; portable off AWS
ECS Fargate — AWS containers, no nodes to manage
low
✔ tasks are long-lived
~$110
closest rival the greenfield pick for a team without k8s skills; no $73 control-plane fee, but AWS-only and our infra code would need a rewrite
cheapest at pilot wins on spiky traffic — ours is steady school-hours load, where always-on is cheaper; still right for cron-shaped work later (EventBridge)
App Runner — container PaaS
very low
~ long-lived, but scale-to-zero reintroduces cold starts
~$60
too cramped clunky VPC model, no cron jobs — we'd bolt Lambda on anyway; fine for one small service, not a platform
Elastic Beanstalk — legacy PaaS on EC2
medium
✔
~$90
skip maintenance-mode energy, awkward with Terraform; nothing it does that ECS/EKS don't do better
Plain EC2 — VMs + Docker Compose
high (patching, SSH deploys)
✔
~$60
demo only no self-healing, no rolling deploys; the §8 runbook becomes slow and manual
skip PaaS options conflict with the VPC/KMS/WAF design; Nomad's ecosystem is too small to bet on
Reading the table: Lambda's ~$40 looks tempting until the workload
shape matters — steady school-hours traffic favors always-on compute, and the §4 connection
manager assumes processes that live longer than a request. ECS Fargate is the honest runner-up;
EKS wins on what already exists (skills, Terraform, runbooks), not on elegance.
Revisit trigger: if cluster upkeep ever consumes >10% of team time, trial ECS Fargate
for the API tier before adding more k8s surface.
🏫 स्कूल प्लॅटफॉर्म — टेनन्सी, सुरक्षा, बॅकअप आणि DR
एकाच डोमेनवरून अनेक शाळा चालवण्याचे प्रोडक्शन डिझाईन — प्रत्येक शाळेसाठी
स्वतंत्र isolation (तुमचा पर्याय E, पुढे F), NestJS बॅकएंड,
तासागणिक→दैनिक बॅकअप lifecycle आणि सराव करून तपासलेली disaster-recovery योजना.
आर्किटेक्चरआत्ता पर्याय E (प्रत्येक शाळेचा silo + control plane) — cross-school analytics लागेल तेव्हा F चे aggregation gate जोडू
टेनन्सी routingएकच डोमेन, subdomain नाही — बाहेरील auth service चा token फक्त ओळख देतो; सक्रिय शाळा X-School-Id header मधून, प्रत्येक request ला membership-तपासणीसह
बॅकएंडNestJS: auth व roles साठी guards, tenant ओळखण्यासाठी middleware, प्रत्येक शाळेसाठी वेगळा connection pool
बॅकअपतासागणिक (१० दिवस) → दैनिक (३० दिवस) → मासिक (१२ महिने) · RPO ≤ १ तास, restore drills
1 आर्किटेक्चर: आज E, analytics लागेल तेव्हा F
E आणि F हे प्रतिस्पर्धी नाहीत — F म्हणजे E + एक जास्तीचा घटक
(governed aggregation gate). त्यामुळे प्रश्न "कोणता?" नसून "कोणत्या क्रमाने?" एवढाच आहे.
सुरुवात E ने — प्रत्येक शाळेचा silo + control plane
प्रत्येक शाळेसाठी स्वतंत्र डेटाबेस — स्वतःचा schema, स्वतःची credentials, स्वतःची बॅकअप लाईन. शाळा A च्या connection ला शाळा B चा डेटा वाचणे शक्यच नाही.Isolation रचनेतच आहे — WHERE clause मध्ये नाही.
Control plane — auth, शाळांची नोंदवही (catalog), routing, feature flags. छोटे, साधे, स्वतःच्या डेटाबेससह.
एकच shared app fleet — तेच NestJS pods सर्व शाळांना सेवा देतात; पडताळलेली ओळख + तपासलेला X-School-Id header ठरवतो कोणत्या शाळेच्या डेटाबेसकडे जायचे.
F चे aggregation gate नंतर जोडा
कधी बांधायचे: cross-school माहितीची पहिली खरी गरज (benchmarks, प्लॅटफॉर्म dashboards, feature-वापराचे आकडे).
फक्त aggregates, एकाच दिशेने — scheduled job शाळा-पातळीवरील aggregates (संख्या, सरासरी) वेगळ्या analytics store मध्ये मोजतो. Row-level डेटा silo बाहेर कधीच जात नाही.
Governance अंगभूत: किमान cohort आकार (उदा. ५ पेक्षा कमी शाळांचा aggregate दडपणे), प्रत्येक aggregate query चा audit log, ad-hoc row egress मार्ग नाही.
निर्णय: E लाँच करा; प्रत्येक silo चा schema सारखाच ठेवा (सर्वत्र त्याच migrations) — म्हणजे F gate हे निव्वळ add-on राहते, refactor नाही.
2 एकच डोमेन, subdomain नाही — UI → API ला शाळा कशी कळते
सर्व काही app.yourschools.com वर चालते. Auth service बाहेरील आहे —
त्याच्या token च्या claims वर आपले नियंत्रण नाही — म्हणून तो token फक्त कोण आहे हे सिद्ध
करतो. कोणती शाळा सक्रिय आहे हे X-School-Id header मधून येते, आणि API तो header
प्रत्येक request ला membership टेबलाशी तपासते. Header हा फक्त इशारा; आपले control plane
हाच अधिकार.
1लॉगिन (बाहेरील)Auth service च्या hosted login कडे redirect. तो त्याचा JWT देतो — फक्त ओळख: sub, email.
2Token पडताळणीAPI त्या service च्या JWKS ने तपासते: signature, issuer, audience, expiry. Custom claims ची गरजच नाही.
3Membership शोधControl plane sub → शाळा + प्रत्येकीतील role देते. हा आपला डेटा — token ला शाळांची माहितीच नसते.
4School picker१ शाळा → आपोआप. २+ → picker (§3 पहा). निवड X-School-Id header बनते.
5प्रत्येक API callUI token + X-School-Id पाठवते. TenantGuard membership पुन्हा तपासतो (~६० से cache) — सदस्य नसल्यास 403.
6Silo connectionCatalog school_id → DB host + secret देतो; त्या शाळेचा cached pool query चालवतो.
तीन स्रोत, तीन कामे
// 1. AUTH SERVICE च्या token मधून (claims वर आपले नियंत्रण नाही):
{ "sub": "auth0|8123", "email": "priya@dps.edu", "exp": ... } // कोण
// 2. आपल्या CONTROL PLANE मधून (प्रत्येक request ला, ६० से cache):
memberships: auth0|8123 → [ { school: "sch_dps_pune", role: "school_admin" },
{ school: "sch_svm_mumbai", role: "teacher" } ] // परवानगी + भूमिका
// 3. CLIENT कडून (तपासल्याशिवाय अविश्वसनीय):
X-School-Id: sch_dps_pune // कोणती — TenantGuard प्रत्येक request ला तपासतो
पक्के नियम
X-School-Id header हा अविश्वसनीय input — TenantGuard प्रत्येक request ला (sub, school) membership टेबलाशी तपासतो; सदस्य नसलेल्याला handler पर्यंत पोहोचण्याआधीच 403. तपासणीशिवाय header वर विश्वास ठेवणे — हाच cross-tenant bug ठरला असता.
Roles आपल्या membership टेबलामधून येतात, token मधून कधीच नाही — auth service फक्त ओळख सिद्ध करते; authorization पूर्णपणे आपले.
JWT claims पेक्षा बोनस: एखाद्याचा access काढणे = membership row delete करणे — ६० सेकंदांच्या cache मध्येच लागू; token expire होण्याची वाट नाही.
प्रत्येक DB access tenant-scoped connection मधूनच जाते — request कोडला सर्व शाळा दिसू शकणारा "global" pool अस्तित्वातच नाही.
3 दोन शाळांमध्ये असलेले युजर्स
एक शिक्षक दोन शाळांत शिकवू शकतो; एक संचालक पाच शाळा चालवू शकतो. नियम:
एका session मध्ये एकच सक्रिय शाळा — dashboard उघडण्यापूर्वी स्पष्टपणे निवडलेली.
प्रवाह
लॉगिननंतर memberships.length > 1 असल्यास UI /choose-school वर नेते — प्रत्येक शाळेचे कार्ड (नाव, लोगो, तिथली role).
निवड सक्रिय शाळा ठरवते: POST /me/active-school ने शेवटची-निवड जतन होते, आणि पुढील प्रत्येक request त्या मूल्यासह X-School-Id पाठवते — जी server दर वेळी membership-तपासतो.
त्यानंतरच app dashboard चा डेटा आणते. या बिंदूनंतर "शाळा निवडलेली नाही" अशी स्थितीच उरत नाही.
Top-nav मध्ये शाळा बदलण्याचा switcher असतो; बदल header चे मूल्य बदलतो आणि dashboard पूर्ण reload होते (शाळा B सक्रिय असताना शाळा A चा कोणताही डेटा स्क्रीनवर उरत नाही). चुकीचे/शिळे header मूल्य काहीही leak करू शकत नाही — प्रत्येक request ची तपासणी त्याला 403 देते.
त्रास टाळणारे तपशील
शेवटची निवड लक्षात ठेवा (control plane मध्ये), ती preselect करा — पण multi-school युजरना दिवसाच्या पहिल्या लॉगिनला picker दाखवाच.९५% वेळा जलद, audit साठी स्पष्ट.
Roles प्रत्येक membership नुसार — तीच व्यक्ती शाळा A त school_admin आणि शाळा B त फक्त teacher असू शकते. Token फक्त सक्रिय शाळेची role वाहतो.
Deep links: "चुकीची" शाळा सक्रिय असताना उघडलेली लिंक "हे पाहण्यासाठी DPS पुणे वर स्विच करा" असे एक-क्लिक interstitial दाखवते — कधीही आपोआप स्विच होत नाही.
4 NestJS blueprint
हे डिझाईन NestJS ला चपखल बसते: authn/authz साठी guards, tenant ओळखण्यासाठी
middleware, आणि प्रत्येक शाळेच्या connection साठी dependency injection. एकमेव अवघड भाग —
pool-per-school — connection manager ने (LRU-capped) सोडवला आहे.
// tenant.guard.ts
const { sub } = await verifyIdpJwt(req); // बाहेरील auth service चे JWKS
const schoolId = req.headers['x-school-id'];
const member = await this.memberships
.check(sub, schoolId); // control plane, ६० से cache
if (!member) throw new ForbiddenException();
req.tenant = { schoolId, role: member.role,
conn: await this.connections.forSchool(schoolId) };
// connection.manager.ts
async forSchool(id: string) {
if (this.pools.has(id)) return this.pools.get(id);
const cfg = await this.catalog.dbConfig(id);
// ↑ host + creds — Secrets Manager, ५ मि cache
const pool = createPool({ ...cfg, max: 5 });
this.pools.set(id, pool); // ~200 नंतर LRU-evict
return pool;
}
छोटे pools × अनेक शाळा — प्रति शाळा ५ connections; schools × pools Postgres च्या मर्यादेजवळ आल्यावर समोर PgBouncer.
सर्व silos साठी migrations — एकच command catalog फिरून प्रत्येक शाळेवर migrations चालवते; नवीन शाळेला त्या onboarding च्या वेळीच मिळतात.
5 API सुरक्षा
AuthN: बाहेरील auth service कडे सोपवलेले — त्याचा JWT आपण JWKS ने तपासतो (signature, issuer, audience, expiry); JWKS keys cache होतात. Token आयुष्य + refresh हे auth service चे काम.
AuthZ (पूर्णपणे आपले): दोन थर — role guard (@Roles('school_admin'), role आपल्या membership टेबलामधून) आणि tenant guard (तपासलेला X-School-Id membership check). दोन्ही प्रत्येक route वर चालतात.
Validation: global ValidationPipe (whitelist: true, forbidNonWhitelisted: true) — अनोळखी fields नाकारली जातात, दुर्लक्षित होत नाहीत.
Rate limiting:@nestjs/throttler — /auth/* वर कडक (brute-force), इतरत्र प्रति-युजर सैल; शिवाय ALB वर AWS WAF (SQLi/XSS rules, IP throttles).
Headers व CORS: helmet defaults; CORS एकाच डोमेनवर लॉक — subdomain न वापरण्याचा बोनस फायदा.
Secrets: env files किंवा images मध्ये काहीही नाही — boot च्या वेळी AWS Secrets Manager मधून (आणि connect करताना प्रत्येक शाळेची DB creds).
Audit log: control-plane टेबल — कोण, शाळा, कृती, entity, वेळ, IP — प्रत्येक बदल व auth घटनेसाठी; अपरिवर्तनीय (UPDATE/DELETE grants नाहीत).
Dependencies: ECR scan-on-push (आधीच चालू) + CI मध्ये npm audit/pip-audit.
6 डेटाबेस सुरक्षा
नेटवर्क: डेटाबेस फक्त private subnets मध्ये; security group फक्त EKS node/pod SG कडूनच port 5432 स्वीकारतो. Public endpoint कधीच नाही.
Encryption: at rest KMS ने (प्रति-instance keys; premium शाळांना स्वतःची CMK), in transit सक्तीचे TLS (client वर rejectUnauthorized).
प्रत्येक शाळेची स्वतःची credentials: प्रत्येक silo चा DB user फक्त स्वतःच्याच डेटाबेसमध्ये जाऊ शकतो — app च्या मार्गात superuser नाही. Secrets Manager rotation ३०–९० दिवसांनी.
किमान-अधिकार grants: app user ला फक्त DML (SELECT/INSERT/UPDATE/DELETE); DDL फक्त migration job च्या वेगळ्या role मधून.
मानवी access: shared psql passwords नाहीत — break-glass साठी SSM Session Manager + IAM auth; प्रत्येक session लॉग होते.
Audit व detection: DDL + role बदलांवर pgAudit; RDS API calls वर CloudTrail; failed-login वाढ व असामान्य egress वर alarms.
शेवटची फळी isolation: bug गेला तरी शाळा-A चे connection शाळा-B ला स्पर्श करणारी query लिहूच शकत नाही — कोणत्याही एका चुकीचा फटका एका शाळेपुरताच.
7 बॅकअप धोरण — तुमचेच retention नियम, अंमलात
गरज: अलीकडच्या इतिहासासाठी तासागणिक बॅकअप नेहमी हजर; १० दिवसांनंतर फक्त दैनिक
ठेवायचे आणि तासागणिक काढून टाकायचे. अंमलबजावणी: प्रति-शाळा dumps S3 वर + lifecycle rules,
आणि खाली RDS point-in-time recovery.
स्तर
कधी
किती काळ
यंत्रणा
Restore
PITR
सतत (WAL)
१० दिवस
RDS automated backups
गेल्या १० दिवसांतील कोणताही क्षण — तासागणिकपेक्षाही बारीक
Bucket: versioned, KMS-encrypted, हवे असल्यास Object Lock (छेडछाड-रोधक), DR region मध्ये replicate (§8).
तडजोड नसलेल्या गोष्टी
प्रति-शाळा फाईल्स — शाळा #42 restore करताना इतर silos ना धक्काही लागत नाही (हाच पर्याय E चा मूळ हेतू).
Backup monitoring: CronJob प्रत्येक शाळेसाठी success marker लिहितो; ताजा backup नसलेल्या शाळेवर रोजचा alert — गुपचूप fail होणारा backup हाच DR चा खरा शत्रू.
Restore drills: दर महिन्याला एका random शाळेचा dump scratch instance मध्ये restore करून row-count तपासणी. कधीही restore न केलेला backup म्हणजे आशा — backup नव्हे.
8 Disaster-recovery योजना
लक्ष्ये: RPO ≤ १ तास (तुमच्या बॅकअप नियमानुसार; PITR मुळे प्रत्यक्षात
मिनिटे) आणि परिस्थितीनुसार RTO. सगळी infrastructure Terraform मध्ये असल्याने सर्व काही
code + backups मधून पुन्हा उभे राहते.
परिस्थिती
कोण हाताळते
RTO
कसे
Pod / node क्रॅश
आपोआप
सेकंद–मिनिटे
Kubernetes self-healing + HPA; multi-AZ node group
Availability-zone बंद
आपोआप
~१–२ मिनिटे
RDS Multi-AZ failover; pods दुसऱ्या AZ त; ALB दोन्ही AZ त असतो
चुकीचे deploy
runbook
मिनिटे
kubectl rollout undo — आधीची image एका command वर (SHA-tagged images)
डेटा बिघाड / चुकीचे migration (एक शाळा)
runbook
< १ तास
PITR ने बिघाडाच्या अगदी आधीच्या क्षणी, किंवा त्या शाळेचा hourly dump नव्या DB त restore करून catalog मधील नोंद बदलणे
Ransom / डेटाशी छेडछाड
runbook
तास
versioned, cross-region S3 मधून restore (Object Lock इतिहास जपतो)
अख्खा region बंद
runbook
< ४ तास
खालील runbook ↓
Region गेल्यावरचा runbook (वर्षातून दोनदा सराव)
१. जाहीर करा — एक व्यक्ती incident commander; सर्वप्रथम status page अपडेट.
२. उभारणी — DR region वर terraform apply (तोच code, फक्त aws_region वेगळा). VPC + EKS + ECR साठी ~२० मिनिटे.
३. Images — ECR cross-region replication मुळे images आधीच तिथे असतात.
४. डेटा — replicate झालेल्या S3 backups मधून प्रत्येक शाळा restore (parallel jobs, catalog-नुसार; मोठ्या शाळा आधी).
५. Cutover — Route 53 तोच एक डोमेन नव्या ALB कडे वळवते (TTL 60 सेकंद ठेवलेला). एक डोमेन = एकच DNS बदल.
६. पडताळणी — प्रत्येक शाळेची smoke test (लॉगिन, dashboard, एक write), मग लेखी timeline सह incident बंद.
हे का चालते: replicate होणाऱ्या तीनच गोष्टी — Terraform code (git), images (ECR replication), डेटा (S3 cross-region) — म्हणजेच संपूर्ण system. बाकी सगळे त्यातून उभे राहते.
9 प्रत्येक शाळेचे ॲडमिन पॅनेल व feature नियंत्रण
दोन वेगळे admin पृष्ठभाग, काटेकोरपणे वेगळे: शाळेच्या स्टाफसाठी school admin
panel आणि तुमच्यासाठी platform panel. Feature flags control plane मध्ये असतात —
त्यामुळे deploy न करता शाळेनुसार features चालू/बंद करता येतात.
भूमिका (प्रत्येक membership नुसार)
भूमिका
व्याप्ती
काय करू शकते
platform_admin
तुम्ही / तुमची टीम
शाळा onboard करणे, plans व feature flags ठरवणे, aggregates पाहणे (फक्त F-gate मधून) — सामान्य कामात शाळांचा row-डेटा कधीच वाचत नाही
school_admin
एक शाळा
त्या शाळेचे users व roles, branding, plan च्या मर्यादेत features चालू/बंद, शाळेचा audit log पाहणे
६. Admin panels: आधी school admin (users + feature toggles), मग platform panel (onboarding automation).
७. पहिली DR drill — एक शाळा restore + region-runbook चा tabletop सराव. यानंतरच platform ला production-ready म्हणा.
८. नंतर: cross-school analytics ची मागणी आल्यावर F चे aggregation gate.
11 आर्किटेक्चर आकृती, DevOps व खर्च
संपूर्ण system एका चित्रात, ती deploy करणारी pipeline, आणि तीन आकारांवर —
pilot, production व scale — दरमहा खर्च किती.
आर्किटेक्चर एका दृष्टीतएकच request मार्ग: युजर → Route 53 → WAF/ALB →
NestJS pods → control plane शी membership तपासणी → त्या शाळेचा silo (§2). ओळख बाहेरील auth
service कडून, JWKS ने पडताळलेली. CronJobs silos वाचून प्रति-शाळा dumps S3 वर लिहितात (§7).
Region गेल्यास Route 53 तोच डोमेन DR region च्या ALB कडे वळवते (§8).
Compute पर्यायांची तुलना — EKS का
पर्याय
Ops भार
Pool-per-school (§4)?
Pilot base / महिना
निर्णय
EKS — managed Kubernetes
मध्यम–जास्त
✔ दीर्घायुषी pods pools सांभाळतात
~$175
निवडले टीम, repo, Terraform आणि §8 चा DR runbook आधीच k8s-आकाराचे; AWS बाहेरही नेता येते
ECS Fargate — AWS containers, nodes सांभाळावे लागत नाहीत
कमी
✔ tasks दीर्घायुषी
~$110
सर्वात जवळचा स्पर्धक k8s अनुभव नसलेल्या टीमसाठी greenfield निवड; $73 control-plane फी नाही, पण AWS-only आणि आपला infra code पुन्हा लिहावा लागेल
pilot ला सर्वात स्वस्त तुरळक/उसळत्या traffic ला जिंकते — आपला load शाळेच्या वेळांत स्थिर, तिथे always-on स्वस्त; cron-आकाराच्या कामासाठी पुढे योग्य (EventBridge)
App Runner — container PaaS
अत्यल्प
~ दीर्घायुषी, पण scale-to-zero मुळे cold starts परत
~$60
अपुरे VPC मॉडेल गैरसोयीचे, cron jobs नाहीत — Lambda जोडावेच लागले असते; एका छोट्या service ला ठीक, platform ला नाही
Elastic Beanstalk — EC2 वरचे जुने PaaS
मध्यम
✔
~$90
वगळा maintenance-mode अवस्था, Terraform शी गैरसोयीचे; ECS/EKS पेक्षा चांगले असे काहीच नाही
साधे EC2 — VMs + Docker Compose
जास्त (patching, SSH deploys)
✔
~$60
फक्त demo self-healing नाही, rolling deploys नाहीत; §8 चा runbook संथ व हाताने करावा लागतो
Lightsail — ठरलेल्या किमतीची bundles
कमी
✔
~$40
वगळा IAM/VPC जोडणी कमकुवत — §6 ची private-subnet + KMS शिस्त पाळता येत नाही
AWS बाहेर — Render / Fly / Railway · Nomad
कमी
✔
बदलते
वगळा PaaS पर्याय VPC/KMS/WAF डिझाईनशी विसंगत; Nomad चे ecosystem पैज लावण्याइतके मोठे नाही
ठोकताळा: AWS वर नवी सुरुवात + Kubernetes अनुभव नाही →
ECS Fargate; k8s मध्ये आधीच गुंतवणूक किंवा multi-cloud ची इच्छा → EKS. आपण नेमके दुसऱ्या
गटात. हा निर्णय प्रति-workload आहे, सरसकट नाही: API fleet EKS वरच राहते, आणि cron-आकाराचे
काम (बॅकअप, freshness checks) पुढे core design ला धक्का न लावता EventBridge + Lambda कडे
नेता येते.
DevOps पद्धत
सर्व काही code मध्ये
सगळी infrastructure Terraform च्या मालकीची — VPC, EKS, RDS, S3 lifecycle, WAF, Route 53. State S3 मध्ये, DynamoDB locking सह; AWS console फक्त वाचण्यासाठी.
Kubernetes manifests (Helm) त्याच repo मध्ये — इतर code प्रमाणेच review व version होतात.
शाळेचे onboarding हा pipeline job, checklist नाही: DB + user + secret तयार, migrations, catalog मध्ये नोंद.
production, sandbox व DR region साठी तेच modules — फक्त tfvars वेगळे. यामुळेच §8 चा runbook म्हणजे एक variable बदल — आठवणीतून पुनर्बांधणी नाही.
Environments
फक्त दोनच cloud environments. Local dev म्हणजे laptop वरचे docker-compose — मोफत, deploy केलेले environment नव्हे.
sandbox — production च्याच आकाराची पण छोटी प्रतिकृती: single-AZ RDS, spot nodes, दोन demo शाळा. प्रत्येक merge वर आपोआप deploy; restore drills व demos इथेच चालतात.
production — सर्व काही multi-AZ; बदल फक्त pipeline च्या manual gate मधूनच येतात.
DR — कायमस्वरूपी environment नाही: code (git) + replicate झालेल्या images (ECR) + बॅकअप (S3) हीच संपूर्ण system — त्यामुळे कायम खर्च जवळपास शून्य.
1Push / PRCI मध्ये lint, unit tests, npm audit — लाल असल्यास merge अडते.
2BuildDocker image ला git SHA चा tag — कधीही latest नाही; rollback ला नेमके tags लागतात.
3PublishECR वर push; scan-on-push गंभीर CVEs अडवते.
4Sandboxआपोआप deploy; migrations catalog फिरून प्रत्येक शाळेवर; smoke suite चालते.
5मंजुरीएक-क्लिक manual gate — merge आणि prod मधली एकमेव मानवी पायरी.
Dashboards: एक platform overview + school_id नुसार प्रति-शाळा drill-down.
खर्च किती (दरमहा, ap-south-1)
घटक
Pilot — ~५ शाळा
Production — ~५० शाळा
Scale — ~२०० शाळा
EKS control plane
$73
$73
$73
Worker nodes (EC2)
2 × t3.medium — $67
3 × t3.large — $190
6 × m6g.large — $420
RDS Postgres
db.t4g.micro, 1-AZ — $15
db.t4g.medium multi-AZ + 100 GB — $145
2 × r6g.large multi-AZ + 500 GB — $850
ALB + WAF
$25
$45
$80
NAT gateway
$36
$40
$70
S3 बॅकअप (+ Glacier)
$3
$15
$50
Secrets Manager
$3
$25
$85
CloudWatch · Route 53 · ECR
$10
$27
$60
Sandbox environment
—
$80
$120
एकूण ≈
$230 / महिना (~₹१९ हजार)
$640 / महिना (~₹५४ हजार)
$1,810 / महिना (~₹१.५ लाख)
प्रति शाळा ≈
$46
$13
$9
खर्च कमी करण्याचे मार्ग (मोठा आधी)
Database-per-school ≠ instance-per-school — अनेक शाळांचे silo databases एकाच RDS instance वर राहतात, तरी पर्याय E चे isolation कायम (वेगळा DB + वेगळी credentials). मोठ्या शाळांना गरज पडेल तेव्हाच स्वतंत्र instance.
Savings Plans / reserved instances — वापर स्थिर झाल्यावर EC2 + RDS वर ३०–४०% सूट.
Graviton (t4g / m6g / r6g) — टेबलात आधीच गृहीत, x86 पेक्षा ~२०% स्वस्त.
Spot nodes — sandbox व backup CronJobs साठी.
S3 / ECR साठी VPC endpoints — NAT चा data-processing खर्च कमी होतो.
§7 चे lifecycle नियम हेही खर्च-नियंत्रणच — तासागणिक १० दिवसांत जातात, ६० दिवसांनी Glacier.
खर्चावरची कुंपणे
AWS Budgets — मासिक लक्ष्याच्या ८०% व १००% वर alerts; anomaly detection चालू.
Cost-allocation tags सर्वत्र (env, जमेल तिथे school_id) — मग Cost Explorer प्रति शाळा खर्च दाखवतो.
आठवड्याला खर्चाचा आढावा हा ops चाच भाग — uptime सारखा. लक्ष ठेवायचा आकडा म्हणजे प्रति-शाळा खर्च: शाळा वाढतील तसा तो घटत गेला पाहिजे.
हे अंदाज आहेत, कोटेशन नाही: ऑगस्ट २०२६ चे ap-south-1 on-demand दर (~₹८४/$),
data transfer, डोमेन व support plan वगळून. पक्के करण्यापूर्वी AWS Pricing Calculator मध्ये
पुन्हा तपासा.
12 Compute पर्यायांची तुलना — EKS का?
NestJS fleet चालवण्याचे AWS वरील सर्व मार्ग, याच डिझाईनच्या कसोटीवर:
pool-per-school connection manager (§4) ला दीर्घायुषी processes लागतात, security posture (§6) ला
खरे VPC/KMS integration लागते, आणि DR runbook (§8) declarative redeploys गृहीत धरतो.
खर्च = ढोबळ pilot-स्केल on-demand आकडे, प्रति महिना.
पर्याय
Ops भार
Pool-per-school (§4)?
Pilot / महिना
निर्णय
EKS — managed Kubernetes
मध्यम–जास्त
✔ दीर्घायुषी pods pools धरून ठेवतात
~$175
निवडले टीम, repo, Terraform आणि §8 चा DR runbook आधीच k8s-आकाराचे आहेत; AWS बाहेरही नेता येते
ECS Fargate — AWS containers, nodes सांभाळायचे नाहीत
कमी
✔ tasks दीर्घायुषी असतात
~$110
सर्वात जवळचा स्पर्धक k8s कौशल्य नसलेल्या टीमसाठी greenfield निवड; $73 control-plane फी नाही — पण AWS-only, आणि आपला infra code पुन्हा लिहावा लागेल
pilot ला सर्वात स्वस्त spiky traffic वर जिंकते — आपला load शाळेच्या वेळांचा, स्थिर आहे, तिथे always-on स्वस्त पडते; cron-आकाराच्या कामांसाठी (EventBridge) पुढे योग्यच
App Runner — container PaaS
अगदी कमी
~ दीर्घायुषी, पण scale-to-zero ने cold starts परत येतात
~$60
अपुरे VPC model गैरसोयीचे, cron jobs नाहीत — शेवटी Lambda जोडावेच लागेल; एका छोट्या service साठी ठीक, प्लॅटफॉर्मसाठी नाही
Elastic Beanstalk — जुने PaaS (EC2 वर)
मध्यम
✔
~$90
वगळा maintenance-mode अवस्था, Terraform शी अवघड; ECS/EKS पेक्षा चांगले असे काहीही नाही
साधे EC2 — VMs + Docker Compose
जास्त (patching, SSH deploys)
✔
~$60
फक्त demo self-healing नाही, rolling deploys नाहीत; §8 चा runbook हळू आणि manual होतो
Lightsail — ठराविक किंमतीचे bundles
कमी
✔
~$40
वगळा IAM/VPC integration कमजोर — §6 ची private-subnet + KMS posture पूर्ण करू शकत नाही
AWS बाहेर — Render / Fly / Railway · Nomad
कमी
✔
वेगवेगळे
वगळा PaaS पर्याय VPC/KMS/WAF डिझाईनशी विसंगत; Nomad ची ecosystem पैज लावण्याइतकी मोठी नाही
टेबल कसे वाचावे: Lambda चे ~$40 आकर्षक वाटते — जोपर्यंत workload चा
आकार लक्षात घेत नाही: शाळेच्या वेळांचा स्थिर traffic always-on compute ला अनुकूल आहे, आणि §4 चा
connection manager request पेक्षा जास्त जगणाऱ्या processes गृहीत धरतो. ECS Fargate हा प्रामाणिक
उपविजेता; EKS जिंकते ते आधीच असलेल्या गोष्टींवर (कौशल्ये, Terraform, runbooks) — शोभेवर नाही.
पुनर्विचाराचा निकष: cluster सांभाळण्यात टीमचा >10% वेळ जाऊ लागला, तर आणखी k8s वाढवण्याआधी
API tier साठी ECS Fargate चाचणी करा.