Building in public, one commit at a time.
Real-time notes from building Uploy - an open-source platform for deploying apps to your own servers, built with Go and Svelte.
#1 Why I Chose Go and SvelteKit
March 1, 2026
The first journal entry about why I chose Go and SvelteKit to build my open source PaaS.
#2 How I Learned Go and SvelteKit
March 2, 2026
A short note about how I started learning Go and SvelteKit before building my open source PaaS.
#3 Trying docker ps from Go to the Frontend
March 2, 2026
A small build log about executing docker ps in Go and showing the result in the frontend.
#4 Trying docker pull nginx from Go to the Frontend
March 2, 2026
Running docker pull nginx:latest from Go to the frontend... and discovering the first waiting problem.
#5 Running RunNginx in the Background with a Goroutine
March 2, 2026
Running docker pull nginx:latest in a background goroutine and returning an instant response to the frontend.
#6 Connecting Go to PostgreSQL with pgx
March 3, 2026
Integrating the Go backend with PostgreSQL using pgx.
#7 Storing RunNginx status in the deployments table
March 3, 2026
Saving background job status into PostgreSQL so the frontend can track deployment state.
#8 Refactoring database code out of main.go
March 4, 2026
Moving pgx connection setup and deployment queries into the db package so the backend starts to feel less cramped.
#9 Fixing misplaced context timeout in RunNginx
March 4, 2026
Separating the Docker pull timeout from the database timeout so deployment status updates do not accidentally control the whole job.
#10 Fixing hidden failures in RunNginx and server shutdown
March 5, 2026
Catching panics inside the deployment goroutine and switching to graceful shutdown so database cleanup actually runs.
#11 Replacing a shared pgx.Conn with pgxpool
March 5, 2026
Switching from a single shared pgx.Conn to pgxpool after a fail-first concurrency test showed why one connection is a bad fit for a web server.
#12 Adding deployment logs and a logs handler
March 6, 2026
Storing docker pull output line by line in PostgreSQL and exposing a logs endpoint the frontend can poll incrementally.
#13 From CombinedOutput to line-by-line deployment logs
March 6, 2026
Trying frontend polling first, then realizing real-time deployment logs only work once the backend reads docker output line by line.
#14 Replacing frontend polling with SSE for deployment logs
March 6, 2026
Switching deployment log updates from frontend polling to SSE, while realizing the backend is still polling the database underneath.
#15 Replacing backend DB polling with a pub/sub broker
March 6, 2026
Using an in-memory pub/sub broker so SSE can receive deployment logs in real time without polling PostgreSQL every 500ms.
#16 Closing the race window in SSE log streaming
March 6, 2026
Fixing a small race in the SSE log flow by subscribing before catch-up, supporting Last-Event-ID, and disconnecting slow subscribers cleanly.
#17 Aligning panic recovery with the deployment failure flow
March 6, 2026
Cleaning up the panic recovery path in RunNginx and simplifying the broker lock from RWMutex to Mutex.
#18 Replacing local deploy commands with SSH-based remote deploy
March 16, 2026
Moving deployment execution from local Docker commands to a remote Ubuntu server over SSH, then fixing a send-on-closed-channel panic in the log stream.
#19 Building auth for a multi-tenant PaaS
March 18, 2026
Adding email/password auth, rate limiting, sliding sessions, and GitHub + Google OAuth2 as the foundation for a multi-tenant PaaS.
#20 Improving DX by moving inline migrations into versioned SQL files
March 18, 2026
Extracting automatic inline migrations from the Go server into versioned SQL files with goose, so migrations and app startup now have a cleaner boundary.
#21 Improving DX with automatic rebuilds using air
March 18, 2026
Adding air to remove the repetitive stop-and-run loop during Go development, so backend changes now rebuild automatically on save.
#22 Adding sqlc for end-to-end type safety from database to backend
March 19, 2026
Keeping raw SQL while adding sqlc code generation, so backend queries stay aligned with the current database schema and feel safer to maintain.
#23 Making openapi.yaml the source of truth for backend and frontend
March 19, 2026
Generating types from openapi.yaml for both Go and TypeScript, so backend and frontend stay aligned from a single schema instead of drifting apart.
#24 Adding CRUD management for SSH keys
March 20, 2026
Implementing SSH key CRUD with private key validation, while also realizing the current storage still needs encryption for better user privacy.
#25 Adding persistent server management with SSH validation
March 20, 2026
Saving server configs into the database and validating them through an SSH session check, so deployments no longer require re-entering the same server details every time.
#26 Encrypting SSH private keys before storing them in the database
March 21, 2026
Adding encryption for stored SSH private keys so sensitive credentials are no longer saved as plain text in the database.
#27 Simplifying deployment into quick deploy and adding application env management
March 23, 2026
Turning the deploy flow into a one-click action by separating applications from deployments, while also adding environment variable management and uncovering a VM bootstrap step that still hurts UX.
#28 Adding deployment history to the application detail page
March 23, 2026
Adding an endpoint for application deployment history and surfacing it in the UI, so each application now has a clearer record of past deploy attempts.
#29 Adding Traefik, custom domains, and automatic TLS with Let’s Encrypt
March 29, 2026
Adding Traefik as the reverse proxy, enabling custom domains, and bootstrapping VM setup so TLS certificates can be issued automatically through Let’s Encrypt.
#30 Adding SSH key generation inside Uploy
March 30, 2026
Moving SSH key generation into Uploy so users no longer need to run ssh-keygen manually on the control plane before connecting their servers.
#31 Moving domain and TLS state into application_domains
March 30, 2026
Separating per-domain TLS state from application and server data so each domain can have its own lifecycle and status.
#32 Adding progress feedback to the install script
March 30, 2026
Making the one-time install flow feel less silent by adding a spinner in default mode and a --verbose mode for raw output.