← Portfolio Case Study

Legacy Rescue — rewriting an abandoned 8-year-old backend

Klient: Family-run services business (3 retail locations, 5 employees, ~18 000 historical orders) Czas: 4 weeks
Node.js Express MySQL React 15 JWT bcrypt Caddy PM2

Before

  • No backups, no documentation
  • Auth effectively disabled via DEV_MODE=true (expired SaaS trial)
  • 10+ reported bugs unfixable without touching code
  • Previous developers quit after a few days of code analysis

After

  • 21 contract items delivered at 100%
  • Zero frontend regressions after full backend swap
  • Own auth (bcrypt + JWT) — no external dependencies, no subscriptions
  • HTTPS with auto-renewed Let’s Encrypt certificate
Legacy Rescue — abandoned 8-year-old system rewritten to production-ready in 4 weeks. 21/21 contract items delivered.

Context

The client runs a 10-year-old business with three retail points inside shopping malls. Eight years ago they commissioned a custom order-management system from an external agency — it replaced spreadsheets and paper folders and became mission-critical for daily operations across all three locations.

The original development team disappeared a few years after delivery. The client tried hiring several developers to continue the work — each one quit within days of looking at the codebase. When they reached out to me, the system was still running, but with accumulating problems:

  • no backups
  • no documentation
  • hardcoded login configuration pointing at an external SaaS provider (trial had almost certainly expired — authorization was silently bypassed via DEV_MODE=true)
  • a dozen reported bugs that couldn’t be fixed without touching code
  • hosting costs (large dedicated VPS) wildly disproportionate to the business size

The client arrived with a "change plan" document containing 21 functional requirements, asking for one thing: is this system worth developing, or do we rewrite from scratch?

Approach

I proposed a two-stage engagement model rather than jumping in blind.

Stage 1 — Paid technical audit (4 days)

Before touching anything, I analyzed the code and database and delivered a written report:

  • assessment of code and database quality
  • each of the 21 requirements mapped to concrete changes (which files, which tables, effort estimate)
  • a develop vs. rewrite recommendation with justification
  • itemized quote for the implementation stage

Recommendation: extend the existing system. Despite its age the code was readable enough, and the historical data was worth preserving without migration.

Stage 2 — Fixed-price contract with schedule

Based on the audit, I proposed a fixed-price contract with full copyright transfer. Scope broken into 3 groups:

  • A — Removals (4 items): unused modules and fields removed to simplify UX
  • B — Bug fixes (5 items): every reported issue
  • C — New features (12 items): the actual business value

Payment upfront, 5 days for client feedback after delivery, 5 more days for fixes within the original price.

Selected technical decisions

1. Swap the backend, keep the frontend

The original backend (Java Spring Boot + Hibernate ORM) was abandoned. Rather than reviving it, I rewrote it in Node.js / Express while preserving 100% of the API contract: same routes, same JSON shape, same snake_case/camelCase behaviour, same date conversions, same semantics for empty fields. The React 15 frontend didn’t need a single line changed.

Result: zero UI regression risk, all complexity concentrated in one place.

2. Schema-aware generic CRUD

Instead of writing five handlers per table, I implemented a helper crud(router, path, table, options) that:

  • loads the table schema via DESCRIBE at boot
  • auto-converts types (tinyint(1) ↔ boolean, DECIMAL ↔ number, DATE with local timezone)
  • handles snake_case ↔ camelCase mapping
  • exposes beforeSave hooks for business logic

Adding another table with full CRUD is now one line.

3. Zero-downtime database migration

I didn’t stop the old application — I stood the new backend up next to it, pointed it at the same MySQL instance, and only switched DNS / nginx after verification. The old version stayed up as a rollback net for 72h.

4. Auth from scratch instead of reviving Auth0

The paid provider’s trial had long expired. Instead of renewing, I wrote a custom module (bcrypt + JWT + requireRole middleware) against a simple users table. Benefits for the client:

  • zero external dependencies
  • zero subscription fees
  • full control over the account list

5. HTTPS via reverse proxy

Caddy 2 + automatic Let’s Encrypt + http→https redirect — the client gets the padlock in the browser without ever having to think about certificate renewal.

Stack (target)

  • Backend: Node.js 16 + Express + mysql2
  • Auth: bcrypt + jsonwebtoken + express-rate-limit
  • Frontend: React 15 (react-scripts 0.8) — not rewritten
  • Database: MySQL 5.7
  • Process manager: PM2 (systemd autostart on reboot)
  • Reverse proxy / TLS: Caddy 2 + Let’s Encrypt
  • Hosting: Linux VPS

Selected features delivered

  • customer autocomplete by phone/name fragment (aggregated from historical orders)
  • bulk price updates with filter by series/number, percentage and flat-amount modes, preview before commit
  • multi-print of orders in one call (page-break per order via CSS print)
  • stock quantity broken down from a single number into a list of items (e.g. "3m × 3 + 1m") with automatic summing
  • filter for completed orders with bulk mark-as-done and undo
  • colour-coded retail location in the table
  • user roles (admin / sales / warehouse) with per-table writeRoles permissions
  • 2 print templates tailored to the client’s workflow (iterated based on phone photos sent over SMS)

Results

  • 21 contract items — 100% delivered.
  • Zero frontend regressions after the backend engine swap.
  • Database migration without downtime (evening maintenance window, ~15 minutes).
  • HTTPS on the client’s own subdomain instead of an IP address.
  • User passwords hashed (bcrypt cost 10), JWT with 30-day TTL.
  • Database backed up retroactively before every deploy.
  • Same-day client acceptance, agreement to continue collaboration.

Process and soft skills

The part most freelancers skip:

  • Post-deploy availability. Every SMS got a reply within the hour. Every remark — a fix within a day.
  • Iterative print refinement based on phone photos from the owner. Two iteration rounds per print template.
  • Decisions asked for, not imposed. At every ambiguity (role structure, domain format) I asked and documented the answer in writing.
  • Transparent pre-deploy communication. Before larger changes (auth, bulk updates of completed orders) I sent an email heads-up with instructions for staff the next morning.

Challenges and lessons

1. React 15 and React.createClass

Components built with createClass — no hooks, no ES6 classes. Manual this.bind, mixins, state mutation via this.state.x = y (an anti-pattern, but consistent with the surrounding code). The temptation to modernize was strong, but the client wouldn’t see the value.

2. Encoding and MySQL 8 vs 5.7

Local XAMPP (MySQL 5.7) and Docker (MySQL 8) handle caching_sha2_password differently. Fix: ran migration scripts via Node (not CLI mysql), keeping everything on the same client (mysql2).

3. CRLF in .sql files

Migrations edited on Windows had CRLF line endings, which occasionally truncated commands inside the MySQL CLI. Fix: I ran each migration through Node (split on ;, filter comments, query by query).

4. bcrypt 6 requires Node ≥18, prod runs Node 16

Warning about an unsupported version, but functionally fine. Left with a note for the next iteration (OS + Node upgrade).

5. DNS propagation and unclear nameservers

Domain registered with one provider, DNS handled by another. Cache of public resolvers vs. authoritative DNS — I initially misdiagnosed the issue as a propagation delay. Lesson: always query the authoritative nameserver, not just 8.8.8.8.

Further opportunities

A follow-up roadmap grew out of this project: automatic SMS notifications to customers, sales reports, migration to cheaper hosting (potential saving ~2000 PLN/year), and OS + Node modernization. Those are out of scope for the base contract, available as separate stages.

What this case study shows about how I work

  1. I don’t walk into legacy blindfolded. A paid audit as a separate stage protects both sides from unrealistic expectations.
  2. I bill for outcomes, with clear scope and formal acceptance. The client knows exactly what they’re getting, I know exactly what I have to deliver.
  3. I think about the client post-deploy. Hashed passwords, pre-migration backups, production state documentation, operational instructions — things nobody thinks about until something breaks.
  4. I don’t push rewrites "because new is better". React 15 works, MySQL 5.7 works, Node 16 works. Modernizing the stack is a separate business decision, not my technical fetish.
  5. Transparent communication, in plain language. Non-technical clients get emails they can read, without mentally translating from English.

Per § 6 of the NDA covering source code, database, and customer data, this case study omits: the company name, exact location, employee and customer personal data, specific infrastructure parameters, and the contents of the audit report. It focuses only on the process, technical decisions, and qualitative results.

Have a similar problem?

Describe it to AI — it gathers technical context, Artur delivers a quote in 48h.

Start diagnostics