Can you turn a brittle legacy app into a multi-tenant SaaS without rewriting it from scratch? We just did. In four sprints, our team relaunched a seven-year-old e-commerce monolith as a subscription-based platform powered by Angular 19 SSR, Node 20 + Fastify and Terraform Cloud.This post distills everything that worked, what blew up in our faces, and a copy-paste migration checklist. Grab a coffee: 8-minute read.
1 — Why Even Migrate? (Hint: Money & Velocity) ☕
| 
 Metric  | 
 Before  | 
 After  | 
 Delta  | 
|---|---|---|---|
| 
 Monthly Release Cadence  | 
 1 / month  | 
 12 / month  | 
 × 12  | 
| 
 Infra Cost / Tenant  | 
 €165  | 
 €97  | 
 -41 %  | 
| 
 LCP 75th p (field)  | 
 4.1 s  | 
 1.9 s  | 
 -54 %  | 
| 
 Net Promoter Score  | 
 34  | 
 65  | 
 +31  | 
ROI kicker: each 1-second LCP drop boosted funnel conversion by 6 %. Numbers made finance very, very happy.
2 — Audit the Monster in 3 Dimensions 🕵️
Before touching code, we ran a 3-D audit. Score every module 1 → 5:
| 
 Dimension  | 
 5 = Red-Zone Symptoms  | 
|---|---|
| 
 Coupling  | 
 Cross-module imports, fat controllers, tangled AngularJS & jQuery  | 
| 
 Test Coverage  | 
 < 10 % paths exercised  | 
| 
 Rollback Blast Radius  | 
 DB migrations are irreversible, prod config differs from staging  | 
Rule of thumb: anything scoring ≥ 4 goes into the “strangler fig” backlog—decouple after you stabilize the happy path.
3 — Architecture Choice: Modular Monolith + Feature Flags 🚀
Why not micro-services right away?
| 
 Option  | 
 ⏱️ Speed to Ship  | 
 🔒 Tenant Isolation  | 
 👷♂️ Ops Burden  | 
|---|---|---|---|
| 
 Lift-and-Shift Docker  | 
 ⚡ Fast  | 
 😰 Minimal  | 
 😀 Low  | 
| 
 Modular Monolith + Flags  | 
 🔄 Balanced  | 
 🙂 Good  | 
 🟡 Medium  | 
| 
 Micro-services (DDD)  | 
 🐢 Slow  | 
 😎 Great  | 
 🔴 High  | 
We chose Modular Monolith:
- Single repo keeps onboarding trivial.
 - Feature flags let us ship dark features to one tenant at a time.
 - Move to services only when a module outgrows the monolith.
 
4 — Frontend Overhaul: Angular 19 with Native SSR 🖼️
# add server-side rendering in two commands
npx ng add @angular/ssr
npm run build:ssr && npm run serve:ssr
Two lessons learned
- Lazy-hydrated Islands: heavy graphs & charts blew up 
renderApplicationmemory. We wrapped them withngSkipHydrationand hydrated onIntersectionObserver. - TC39 Temporal API: Angular 19’s new date pipes + Node 20 eliminated 30 kB of Moment.js dead weight.
 
Result: LCP < 2 s on real Moto G4 devices.
5 — Backend & Tenancy: Fastify + Postgres RLS 🗄️
- Fastify because 80 k req/s on a single M6g large with zero tuning.
 - Row-Level Security (
policy USING (tenant_id = current_setting('app.tenant_id'))) keeps one DB until we hit 1 TB—then we partition. - Observability: OpenTelemetry → Grafana Cloud; one dashboard per tenant with UID templating.
 
6 — CI/CD: Green-Only Deploys in 45 Lines 📦
# .github/workflows/deploy.yml  (core)
on: [push]
jobs:
  test: …          # npm ci && npm test
  build_ssr: …     # npm run build:ssr
  deploy:
    needs: build_ssr
    runs-on: ubuntu-latest
    permissions: { id-token: write }
    steps:
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init && terraform apply -auto-approve
Prod deploy in 11 min. If tests fail, prod is untouched.
7 — Security First (Really) 🔐
| 
 Layer  | 
 Must-Have Control  | 
 Tooling  | 
|---|---|---|
| 
 Auth  | 
 Passwordless magic-link + OAuth 2.1  | 
 Auth.js & Argon2  | 
| 
 API  | 
 Per-tenant rate-limit + HMAC sigs  | 
 Fastify hooks, Redis  | 
| 
 Data  | 
 AES-256 PII encryption + RLS  | 
 Postgres 15, AWS KMS  | 
| 
 Infra  | 
 CIS Level 1 as code  | 
 tfsec, Open Policy Agent  | 
Fun fact: Week 1, 37 % of traffic was credential-stuffing bots—blocked automatically.
8 — Cost Lever Matrix 💸
| 
 Lever  | 
 Year-1 Savings  | 
 How  | 
|---|---|---|
| 
 Edge Caching  | 
 -23 %  | 
 Cloudflare caches SSR HTML + stale-while-revalidate  | 
| 
 Serverless Cron  | 
 -11 %  | 
 Nightly reports moved to AWS Lambda  | 
| 
 Cloud Credits  | 
 -17 %  | 
 AWS Activate + open-source sponsorship  | 
| 
 Multi-AZ  | 
 +6 % cost  | 
 Worth it: SLA 99.95 % → churn -1.2 %  | 
9 — Five Lessons We Keep Re-Learning 🤹♂️
- Feature flags > long-lived branches.
 - Measure field LCP, not just Lighthouse.
 - Docs or die. Every interface change = one ADR file.
 - Tenant-id on Day 0 – retro-fitting is hell.
 - Post-launch “broken windows” sprint saves morale.
 
10 — Pocket Checklist (Steal Me) ✅
- ☐ Build a coupling matrix
 - ☐ Add 
tenant_idcolumn everywhere now - ☐ Ship risky slices behind flags
 - ☐ Synthetic health check per tenant
 - ☐ Schedule “Fix Broken Windows” sprint after go-live
 
Shipping a SaaS is never one-click magic. But with a modular plan, ruthless DevOps discipline and an obsession for user experience, you can turn a creaky monolith into a growth flywheel in under a month. Share your war stories below—let’s swap scars! 🚀
