Business Problem
The live browser-based multiplayer game depended on a Render application service and a separate paid Render PostgreSQL database. That environment supported Google and X OAuth, persistent player accounts, login sessions, match history, matchmaking, and live game networking.
The migration needed to improve control over the production environment and recurring service costs without sacrificing the application state or real-time behavior players already relied on.
- Remove the separate managed PostgreSQL expense while preserving accounts, compatible active sessions, OAuth behavior, and match history
- Maintain dependable HTTPS, matchmaking, and real-time gameplay
- Establish a safe and repeatable production deployment process
- Improve infrastructure control, backups, verification, and rollback protection
This was not a repository copy. It required coordinated engineering across application architecture, networking, authentication, database conversion, DNS, TLS certificates, browser caching, service workers, deployment automation, and production verification.
Solution Overview
The application was migrated as a coordinated production system to a hardened DigitalOcean droplet. The final design separates conventional HTTPS traffic from WebTransport gameplay, keeps the application service private where appropriate, stores production data locally, and uses service supervision plus immutable releases for controlled operation.
The work joined systems integration, cloud infrastructure, application changes, data conversion, security controls, and browser recovery into one production migration rather than treating them as independent tasks.
HTTPS application path
Real-time gameplay path
Caddy handles TCP ports 80 and 443. Its HTTP/3 listener is disabled so the Rust service can directly own UDP 443 for WebTransport over QUIC.
Original Environment
The former environment used Render for the application service and managed PostgreSQL. The game combined a Rust server, a TypeScript and Vite web client, Google and X OAuth, persistent account and session data, matchmaking, match history, and real-time browser gameplay.
Moving the application therefore required preserving both stored relationships and live production behavior. The original Render database remained available until the migrated environment and converted records were verified.
Target Architecture
The target placed the production stack on a DigitalOcean droplet with deliberately separated responsibilities:
- Caddy listens on TCP ports 80 and 443 for redirects, HTTPS, reverse proxying, security headers, compression, and access logging
- The Rust HTTP service listens privately on TCP port 3010
- The Rust game server directly owns UDP 443 for WebTransport over QUIC
- Caddy HTTP/3 is disabled to prevent a conflict over UDP 443
- A local libSQL/SQLite-compatible file stores production data
- systemd manages the application service and deployment checks
- Immutable release directories and a current-release symlink support controlled activation and rollback
- UFW, fail2ban, an unprivileged service account, protected secrets, daily backups, and automated recovery controls harden operation
Production Server Bootstrap
The droplet bootstrap installed and configured the runtime, build, database, proxy, certificate, firewall, and monitoring prerequisites required by the application.
- Node.js 22, Rust 1.93, SQLite, PostgreSQL command-line tools, Caddy, Certbot, UFW, fail2ban, and supporting build tools
- A 2 GiB swap file and single-job Rust release builds to manage constrained build memory
- An unprivileged service user for normal application operation
- Only the minimal capability required for the Rust service to bind UDP 443
- Root-protected environment files for production secrets
PostgreSQL-to-libSQL Migration
The data layer moved from managed PostgreSQL to a local libSQL/SQLite-compatible database file on the DigitalOcean droplet. Turso Cloud was not used.
The database migration required deliberate schema translation rather than mechanical SQL copying. Differences included data types, constraint behavior, timestamp representation, auto-increment behavior, primary keys, foreign keys, cascading deletes, unique and check constraints, partial indexes, JSON validation, match-history indexes, and session-expiration indexes.
The resulting canonical schema preserved the application relationships and validation behavior required by the Rust data layer.
Live Data Migration
A dedicated migration utility received the temporary PostgreSQL URL through an environment variable and opened a read-only transaction. It exported six tables from one consistent database snapshot into a protected temporary JSON file before creating and loading the canonical local schema.
- Preserved identifiers and relationships during conversion
- Validated row counts, foreign keys, constraints, and JSON data
- Refused to overwrite an existing destination database
- Securely deleted temporary migration data after use
Using one snapshot mattered because related records could otherwise have been captured at different moments while the source system continued operating, creating inconsistent relationships in the destination.
OAuth and Session Continuity
The migration retained the existing Google and X OAuth configuration while establishing canonical callback URLs on the www hostname. DNS changes and canonical redirects were coordinated so providers and browsers reached the correct production origin.
Session-token hashes were preserved with the migrated records. Existing login sessions could continue where their expiration state and browser cookie compatibility allowed; the migration did not claim that every expired or incompatible session would remain active.
HTTPS and WebTransport
Caddy terminates HTTPS traffic over TCP 443 and reverse proxies application requests to the private Rust HTTP service on TCP 3010. The Rust game server separately handles WebTransport over QUIC on UDP 443.
Because TCP and UDP are separate transports, both services can use port 443 for different responsibilities. Caddy HTTP/3 was disabled so it would not compete for UDP 443.
Caddy and Rust use the required Let’s Encrypt certificate files. Certificate renewal runs a hook that reloads Caddy and restarts the Rust service so both paths receive renewed certificate material.
Automated Deployments
A root-protected, read-only fine-grained GitHub token allows a systemd timer to check the designated production branch without exposing the token to the running application.
Each deployment creates an immutable release, installs dependencies, runs TypeScript checking, builds the web client, builds the Rust release with one build job, switches the current-release symlink, restarts the service, and performs a health check. A failed health check automatically restores the previous release and restarts the known prior version.
An authenticated admin-dashboard action can request deployment, but the privileged GitHub token remains outside the application environment. The application requests the controlled operation rather than receiving root credentials.
PWA and Service-Worker Recovery
After the infrastructure migration, some returning users still received the legacy Render-era application shell while incognito sessions worked normally. The difference pointed to an old service worker and CacheStorage state rather than the new server build.
Service workers are origin-specific, and a redirected service-worker update fails. The apex and www origins therefore complicated recovery: clients controlled by the old apex-origin worker could not simply follow the canonical redirect to update themselves.
The recovery worker was served directly from the old apex origin, with same-origin health and version endpoints available to legacy clients. It removed obsolete CacheStorage entries while preserving cookies, OAuth state, and local settings, then allowed the browser to navigate normally to www.
Resolving the issue required comparing live request logs with Git history to identify what returning browsers were requesting and which legacy worker behavior still controlled them.
Backups and Operational Safety
The production database receives a daily online SQLite backup with fourteen-day retention. Online backup produces a consistent snapshot while the live application continues operating.
The original Render database was retained until migration verification was complete. Same-droplet backups improve recovery from application or data mistakes, but they do not protect against total droplet loss; separate off-server backup storage remains the recommended additional safeguard.
Role of Codex
Codex supported the repository architecture audit, production planning, bootstrap and deployment scripts, PostgreSQL schema translation, data-migration utility, systemd and Caddy configuration, TLS, firewall rules, backups, Rust database-layer changes, OAuth and session preservation, deployment automation, troubleshooting, and verification.
This was AI-assisted engineering, not a one-click autonomous deployment. Engineering judgment and sensitive actions remained under human control, including DNS changes, OAuth configuration, GitHub token creation, credential handling, and production decisions.
Technology Stack
Application
- Rust
- TypeScript
- Vite
- Node.js
- Progressive Web Application
- Service Workers
Infrastructure
- DigitalOcean
- Render
- Caddy
- systemd
Data
- PostgreSQL
- libSQL
- SQLite
Networking
- WebTransport
- QUIC
- Certbot
- Let’s Encrypt
Authentication
- Google OAuth
- X OAuth
Security
- UFW
- fail2ban
Deployment
- GitHub
- Immutable Releases
- Automated Rollback
AI-Assisted Engineering
- Codex
Results & Operational Impact
Mercenaries of Tezigdal now runs entirely on a DigitalOcean droplet. The hardened Rust service supports HTTPS with automatic certificate renewal, direct WebTransport gameplay over QUIC, and local libSQL-compatible storage.
- Preserved user accounts, compatible still-valid sessions, OAuth behavior, and match history
- Daily database backups with fourteen-day retention
- Immutable deployments, health checks, automatic rollback, and automated deployment from the designated production branch
- No remaining runtime dependency on Render and removal of the separate paid PostgreSQL service
- Greater control over the complete production stack
The migration improved infrastructure control, deployment repeatability, operational resilience, rollback safety, backup ownership, production observability, cost control, and the ability to diagnose and correct real-world production behavior. No dollar savings, uptime percentage, or performance metric is claimed.