Skip to content
Mahdi Maiza
ImplementedBackend migration and operations portal

CoreAPI — FTP-to-HTTPS migration and operations platform

Migration completed July 2026 · containerized deployment

A Node.js HTTPS API that replaced FTP as the way a legacy desktop client talks to its back office, rolled out client by client, paired with a protected admin portal for service health, storage, backups, diagnostics and controlled operations.

Role
Software Engineer
Dates
Sep 2025 – Jul 2026
Source
Private — case study only

01 · Context

The problem

A Delphi desktop client exchanged files with its back office over FTP. When the service moved to new servers, newer network equipment began dropping FTP connections, and the FTP setup could no longer keep up with a growing client base. Adding more FTP servers would have fixed neither problem. Every client had to move to authenticated HTTPS without a single cut-over day, so FTP-era and migrated clients had to keep working side by side until the last one moved.

Constraints

  • The existing Delphi desktop client had to keep working, over FTP or over the API, throughout the migration.
  • Large files travel over unreliable connections.
  • Clients, service staff and administrators need different access scopes and request budgets.
  • One VPS, provisioned from scratch, hosts both the API stack and a public web server.
  • Operators need restarts, logs and backups without unrestricted host access.

02 · Ownership

What I owned

  • Diagnosed why the legacy client stopped working reliably after a server move: newer network equipment was dropping FTP connections, a weakness of FTP's separate control and data connections rather than of the application.
  • Designed the FTP-to-HTTPS migration and built the Node.js/Express API the Delphi client now uses, including chunked, resumable uploads, downloads and provisioning.
  • Built client-by-client migration: clients switch over with their existing FTP credentials or a one-time secret, each client is tracked as FTP, dual or API, and a compatibility bridge keeps FTP-era clients in sync until they move.
  • Ran the rollout in stages — local testing, a small pilot, then progressively larger client batches — fixing issues before each expansion, until the whole client base had moved.
  • Provisioned the production VPS from the operating system up, including its web server, and deployed the Docker Compose stack: Nginx, API, MySQL, Redis, the admin portal and a restricted Docker socket proxy. Managed the TLS certificates, including a wildcard certificate, and the domain migration.
  • Scaled the API across CPU cores with a Node.js cluster, shared rate limits and metrics between workers through Redis, and kept file storage behind a provider interface, so the step to several servers is shared storage rather than a rewrite.
  • Implemented role- and region-scoped access control and Redis-backed rate limits.
  • Built the SuperAdmin portal (Express and React) for health, diagnostics, logs, backups and controlled service actions.
  • Wrote the SQL schema migrations, structured logging and container health checks.

03 · Architecture

System at a glance

Runtime topology and trust boundaries

  • Implemented

Public edge

  • Desktop clients

    HTTPS only

    Status: Implemented.

  • TLS edge

    The only public listener

    Status: Implemented.

Application

  • Nginx

    Reverse proxy

    Status: Implemented.

  • Node.js API

    Auth, rate limits, transfers

    Status: Implemented.

Data

  • MySQL

    Users, files, audit

    Status: Implemented.

  • Redis

    Rate-limit windows, metrics

    Status: Implemented.

  • File storage

    Transfer volumes

    Status: Implemented.

Operations · loopback only

  • SuperAdmin portal

    Health, logs, backups

    Status: Implemented.

  • Docker socket proxy

    Limited endpoints

    Status: Implemented.

Public traffic reaches the server only through a TLS edge, which forwards to Nginx and then to the Node.js API. MySQL, Redis and file storage sit behind the API on a private container network, and every published container port is bound to the loopback interface. The SuperAdmin portal is also loopback-only and reached through an SSH tunnel; it controls containers only through a socket proxy that exposes a limited set of Docker endpoints and an allowlist of service actions.

How the pieces connect
  1. Desktop clients connect over HTTPS to the TLS edge, which forwards to Nginx and then to the Node.js API.
  2. The API stores users, file records and audit data in MySQL, keeps rate-limit windows in Redis, and writes transfers to file storage.
  3. An operator reaches the SuperAdmin portal through an SSH tunnel, because the portal listens on loopback only.
  4. The portal reads container status and logs, and performs allowlisted restarts, only through the Docker socket proxy.

Stack

  • Node.js
  • Express
  • MySQL
  • Redis
  • Docker Compose
  • Nginx
  • Zod
  • Pino
  • React
  • Vite

04 · Decisions

Engineering decisions

Each decision is written as the constraint that forced it, the choice made, the cost accepted, and what it bought.

  • D01

    Migrate client by client, not on a single cut-over day

    Constraint
    The installed desktop clients could not all be updated at once, and a failed switch would cut a client off from its back office.
    Decision
    Let each client provision itself with the FTP credentials it already had, track every client as FTP, dual or API, and mirror new settings back into the plain-text files that FTP-era clients still read.
    Trade-off
    Two protocols and a compatibility bridge had to stay running and consistent for the whole migration, and the bridge is extra code to retire afterwards.
    Result
    Clients moved in growing batches with no service-wide cut-over, and problems surfaced on a small group before they could reach everyone.
  • D02

    Nothing listens publicly except the edge

    Constraint
    The API runs on the same server as a public web server, and the database, cache and admin tools must never face the internet.
    Decision
    Bind every published container port to loopback, place Nginx between the TLS edge and the API, and keep MySQL, Redis and the admin portal on a private container network.
    Trade-off
    Administrators need an SSH tunnel to reach the portal, and troubleshooting starts on the host.
    Result
    The public attack surface is a single HTTPS entry point; data stores and operations tooling are unreachable from outside.
  • D03

    Operations without handing out the Docker socket

    Constraint
    The portal needs container status, logs and restarts, but raw access to the Docker socket is equivalent to root on the host.
    Decision
    Route Docker calls through a socket proxy that exposes only container and info endpoints — exec, images, volumes and build stay disabled — and permit only named service/action pairs, with typed confirmation for disruptive actions and an operations audit log.
    Trade-off
    Every new operational action needs an explicit allowlist entry and a review, which slows down ad-hoc fixes.
    Result
    Operators can diagnose and restart services from the portal without the portal becoming a path to host takeover.
  • D04

    Rate limits follow the caller's role

    Constraint
    Desktop clients, service staff and super-admins have very different legitimate request volumes, and provisioning is an obvious abuse target.
    Decision
    Sliding-window limits in Redis sorted sets, keyed per authenticated user with a budget per role, plus a separate hourly per-IP limit on provisioning. Refusals include a Retry-After header.
    Trade-off
    Every authenticated request pays an extra Redis round trip, and Redis becomes part of the request path.
    Result
    One noisy client cannot exhaust capacity for everyone else, and well-behaved clients are told when to retry.
  • D05

    Resumable uploads, strict filenames

    Constraint
    Replacing FTP meant large files over unreliable connections, with filenames arriving from many desktop installations — including Arabic script.
    Decision
    Chunked uploads tracked by upload ID, with a status endpoint that reports received and missing chunks and a scheduled sweep for abandoned uploads. Filenames are rejected — never silently rewritten — when they contain control characters, path separators, a leading dot or characters outside a whitelist.
    Trade-off
    Rejecting instead of sanitizing means some legacy filenames must be renamed on the client.
    Result
    Interrupted transfers resume instead of restarting, and path-traversal attempts never reach the filesystem layer.

05 · Operations

Delivery and operations

Health-gated startup
MySQL, Redis, the API and the admin portal all define container health checks, and dependent services wait for healthy dependencies instead of racing them at boot.
Structured logging
JSON logs through Pino, with a request ID assigned to every API request so a client-reported error can be traced to its log lines.
Versioned schema
Numbered SQL migrations cover the transfer schema, messaging, provisioning, and the operations audit and monitoring tables.
Backups and monitoring
The portal runs, downloads and restores database backups, and a watchdog records incidents and sends e-mail alerts, with a test alert that doesn't require waiting for an outage.

06 · Outcome

Outcome

  • By July 2026 the whole client base — about 2,000 clients, more than the FTP setup could serve — was on the API, running on a single server sized with room to grow.

Contact

Building something that has to keep running?

I’m glad to talk about engineering roles, delivery and operations work, or a system you need to make more reliable. Email is the fastest way to reach me.

Open to part-time, remote or student roles