Foxlancer
Modernizing a legacy marketplace without throwing away the business.
A Node.js and React rebuild of a CodeIgniter freelancing marketplace that keeps the existing MySQL database and business rules intact.
Problem
A working freelancing marketplace — jobs, bids, hiring, wallets, milestones, escrow and disputes — ran on an aging CodeIgniter codebase. A big-bang rewrite with a new database would put years of data and business rules at risk.
Context
The legacy application is left untouched as the reference implementation. The new stack reads and writes the same MySQL tables, so both can be compared side by side while modules move across.
Architecture
A strangler-style migration: the database stays, the application layer is replaced module by module, and a migration tracker records parity for each feature.
- Legacy
- CodeIgniter app
- Existing MySQL (serv_* tables)
Kept as reference, never modified
- Modern API
- Express + TypeScript
- Controllers
- Services
- Repositories
- Contracts
- JWT auth
- Zod validation
- REST /api/v1
- Frontends
- Marketplace (React + Vite)
- Admin (React + Vite)
- Experience
- Redesigned UX
- Tailwind
- TanStack Query
Engineering decisions
- Keep the existing MySQL schema
- The data and the rules encoded in it are the business. Reusing the serv_* tables removes a whole class of migration risk and lets the old and new apps run against the same data.
- Layered API: routes → controllers → services → repositories
- Controllers stay HTTP-only, business rules such as hire status transitions live in services, and SQL against legacy tables is isolated in repositories.
- JWT with legacy password compatibility
- Users keep their existing credentials: the API verifies legacy password hashes and applies the same account gates, while issuing modern bearer tokens.
- Zod validators mirroring legacy messages
- Behavioural parity includes the error messages users already know, not just the happy path.
- Separate marketplace and admin frontends, one API
- Mirrors the legacy split so operational workflows do not change while the stack does.
Migration tracking
Every legacy module is tracked for API, UI and parity status. Gaps are listed openly — live PayPal IPN, the hourly tracker, social login and deeper admin tooling are still in progress — so nobody mistakes progress for completion.
Lessons
- Don't rewrite what you don't understand: the legacy app is the specification.
- Parity is measured per module, not declared for the whole system.
- Keeping the database stable turns a risky rewrite into an incremental migration.