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
DESCRIBEat boot - auto-converts types (
tinyint(1)↔ boolean,DECIMAL↔ number, DATE with local timezone) - handles snake_case ↔ camelCase mapping
- exposes
beforeSavehooks 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
writeRolespermissions - 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
- I don’t walk into legacy blindfolded. A paid audit as a separate stage protects both sides from unrealistic expectations.
- 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.
- I think about the client post-deploy. Hashed passwords, pre-migration backups, production state documentation, operational instructions — things nobody thinks about until something breaks.
- 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.
- 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.