[ MODERNIZATION ] // PHP → NODE.JS

Migrate off legacy PHP without a 14-month rewrite or a forced logout.

We move CodeIgniter, Laravel 4-5, and homegrown PHP apps to Node.js (TypeScript, Fastify, NestJS) route-by-route — same MySQL, same sessions, same users. No big-bang rewrites.

Veteran-Owned SDVOSB
[001 / 005] Field Conditions

Most PHP-to-Node rewrites die in month nine with two codebases in production and neither one finished.

// SITUATION

The standard failure pattern: a team decides Laravel 4 is unmaintainable, scopes a 'clean rewrite' in Node, freezes feature work on the PHP side, and 11 months later has a half-finished Node app, an angry product team, and a PHP codebase that's now 11 months further behind. Sessions don't carry over. The new ORM doesn't match the old schema. Composer packages with no Node equivalent get reimplemented from scratch. Meanwhile bugs in the live PHP app still need fixing, by engineers who've mentally checked out.

  • Big-bang rewrites freeze feature delivery for 9-18 months while competitors keep shipping against you.
  • Session formats (Laravel encrypted cookies, CI flashdata) break on cutover, forcing every user to re-authenticate.
  • Eloquent relationships and Composer packages (Nova, Spatie, Filament) have no clean Node equivalent and get under-scoped.
  • PHP-FPM's per-request process model hides memory leaks that surface immediately under Node's long-lived processes.
3-4 wks
to first production route on Node
0
forced password resets at cutover
60-80%
smaller container image post-migration
[002 / 005] Operational Approach

Strangle the PHP monolith route by route — don't big-bang rewrite.

  1. STEP-01

    Inventory routes and DB coupling

    We map every controller, cron, and webhook against actual traffic from access logs. We tag routes by DB write surface, session dependency, and external integrations. The output is a strangler plan — what moves first, what stays PHP for 18+ months, what gets killed.

  2. STEP-02

    Shared session and auth layer

    Before any route moves, we externalize sessions to Redis with a format both PHP and Node can read. PHP's password_hash bcrypt verifies cleanly in Node via bcryptjs. Laravel/CodeIgniter cookies stay valid across both runtimes during cutover.

  3. STEP-03

    Reverse proxy with route-level cutover

    Nginx or a small Fastify gateway routes /api/v2/* to Node while /legacy/* stays on PHP-FPM. We ship one route at a time behind feature flags, run shadow traffic for 48 hours, and roll back at the proxy if error rates spike.

  4. STEP-04

    Same database, new access patterns

    We keep MySQL/Postgres in place and use Prisma or Knex against the existing schema. No data migration on day one. Eloquent's snake_case columns map cleanly. We add migrations to Node's toolchain only after PHP writes to that table are gone.

  5. STEP-05

    Decommission PHP-FPM deliberately

    Once a route has zero PHP traffic for 30 days, we delete the controller and its tests. When the last route falls, we tear down PHP-FPM, Composer, and the deploy pipeline. Most migrations end with a 60-80% smaller container image.

// TYPESCRIPT PATTERN
import bcrypt from 'bcryptjs';
import { createClient } from 'redis';
import { FastifyInstance } from 'fastify';

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

export async function authRoutes(app: FastifyInstance) {
  app.post('/api/v2/login', async (req, reply) => {
    const { email, password } = req.body as { email: string; password: string };

    // Same `users` table Laravel writes to. Same bcrypt cost factor.
    const user = await app.db('users').where({ email }).first();
    if (!user) return reply.code(401).send({ error: 'invalid' });

    // Laravel stores $2y$ — bcryptjs handles it transparently.
    const ok = await bcrypt.compare(password, user.password);
    if (!ok) return reply.code(401).send({ error: 'invalid' });

    // Write the session in Laravel's format so PHP routes still recognize it.
    const sessionId = crypto.randomUUID();
    await redis.setEx(
      `laravel_session:${sessionId}`,
      7200,
      JSON.stringify({ user_id: user.id, _token: crypto.randomUUID() })
    );

    reply.setCookie('laravel_session', sessionId, { httpOnly: true, secure: true });
    return { ok: true };
  });
}

Verifying Laravel/PHP bcrypt password hashes from Node so users stay logged in during cutover — no forced password resets.

[003 / 005] Common Questions

Field FAQ.

Should we even migrate off PHP? Modern PHP 8.3 is fast.

Often the answer is no. PHP 8.3 with OPcache and JIT is genuinely fast, and Laravel 10/11 is a competent framework. We tell roughly a third of clients to upgrade PHP rather than migrate. The migration case is strongest when you need long-running processes (websockets, queues, streaming), share code with a React/Next frontend, or your hiring pipeline has dried up. If your only complaint is 'PHP feels old,' stay on PHP.

How do you handle sessions during the transition period?

We move session storage out of PHP's file handler and into Redis using a key format both runtimes understand. Laravel's encrypted session payload can be decrypted in Node with the APP_KEY and a small AES-256-CBC helper. CodeIgniter sessions are simpler — usually just a serialized PHP array we deserialize with the php-serialize npm package. Users stay logged in across the cutover with zero forced re-authentication, which matters a lot for B2C apps.

Do we need to migrate the database too?

Almost never on day one, and often never at all. MySQL and Postgres are perfectly happy being accessed from Node. We keep the existing schema, existing migrations, and existing data. Prisma or Knex introspect the schema and generate types. The only real friction is Eloquent's soft deletes and polymorphic relations, which we reimplement explicitly in the query layer. Database migration is a separate decision made for separate reasons — usually scale, not language.

What about Composer packages we depend on?

This is where migrations get expensive. Spatie permissions, Laravel Nova, Filament, Backpack — these have no Node equivalent and you'll rebuild them. We inventory Composer dependencies early and price the rebuild cost honestly. If you're deep into Nova or Filament for admin tooling, we usually recommend keeping the admin panel on PHP indefinitely and only migrating the customer-facing API. Splitting along that line saves months.

How long does a typical PHP to Node.js migration take?

For a CodeIgniter 3 or Laravel 5 application with 50-150 routes, expect 4-9 months for full cutover with a team of three to four engineers. The first production route ships in week 3-4. The last 20% of routes — admin, reporting, edge cases — takes 40% of the time. Big-bang rewrites of the same scope usually take 14-24 months and have a much higher failure rate, which is why we don't do them.

What runtime do you recommend — Node, Bun, Deno, or something else?

Node.js with TypeScript and Fastify or NestJS for 90% of cases. It has the deepest ecosystem, the best hiring market, and the most boring deployment story — which is what you want when you're already absorbing migration risk. Bun is fast and we use it for tooling, but production stability and observability tooling still lag Node. Deno is fine for greenfield. Go and Elixir are valid choices when you have specific concurrency needs that justify a smaller talent pool.

Can SDVOSB-certified federal work be done on this stack?

Yes. We're a veteran-owned, SDVOSB-certified firm and we run PHP-to-Node migrations for federal customers under the appropriate contract vehicles. Node.js on a FedRAMP-authorized cloud (AWS GovCloud, Azure Government) is well-trodden ground. We handle the STIG hardening, FIPS-validated crypto modules, and the documentation package that goes with ATO. The migration playbook is the same — the compliance overlay is what changes.

How do you handle deployment differences? PHP-FPM vs Node processes are very different.

PHP's request-per-process model is forgiving — memory leaks die at the end of each request. Node holds long-lived processes, so a leak compounds. We instrument heap usage from day one with clinic.js or 0x, set memory limits in the container, and use PM2 or the orchestrator's restart policy as a backstop. Deploys move from rsync-and-reload-FPM to immutable container images. CI/CD usually gets rebuilt; we won't pretend otherwise.

What does the ROI look like compared to just upgrading PHP in place?

Upgrading PHP 5.6 or 7.x to 8.3 and Laravel 5 to Laravel 11 typically costs 20-40% of a full Node migration and delivers 60-70% of the performance and developer-experience gains. We run that math openly in the first two weeks. If the only real driver is performance or maintainability, the in-place upgrade usually wins. The Node migration wins when you're consolidating with an existing TypeScript codebase or need real-time features PHP handles awkwardly.

[ NEXT ACTION ]

Get a route-by-route migration plan from engineers who've shipped this before.

Talk to a VooStack operator. We respond within one business day.