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 📦
`yaml
.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
 
Get the 28-Page Migration Workbook
Need the detailed budgets, Terraform modules and RLS snippets?
Grab the full PDF here → Novane (free, no email gate).
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! 🚀
              
    
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.