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
- Desktop clients connect over HTTPS to the TLS edge, which forwards to Nginx and then to the Node.js API.
- The API stores users, file records and audit data in MySQL, keeps rate-limit windows in Redis, and writes transfers to file storage.
- An operator reaches the SuperAdmin portal through an SSH tunnel, because the portal listens on loopback only.
- 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.
