Latest project I've been working on is gathering up all odd bits and ends deployed on various PaaSes (Vercel, fly.io, Supabase, ++) and hosting them myself on a VPS. It's been a fun journey learning about setting up a VPS in a way that is reasonable secure to leave exposed to the Internet.
One of the things I'm migrating is this site. And, since I'm trying to get rid of a bunch of my dependencies on various providers' free tiers I'm rewriting the site to get off Supabase. It's currently built with SvelteKit with a Supabase backend, but for ease of deployability I have staerted to port it to Go, backed by a SQLite database, and with simple html/template HTML templates that just renders the pages server-side and servers them to the user. I'm also dropping JS entirely (for now at least) and moving away from the SPA feel. This site is pretty basic anyways, and it feels overkill to use a complete fullstack JS framework for something like this.
So far I've mostly ported the backend. There are still a few things missing from the backend, but nothing huge. Most of the frontend is also in place, but the styling is incomplete.
As is common when rewriting, some parts are changed along the way. So far I've dropped OAuth, opting instead for managing my own users. I'm not going to have open sign-up, but will make it possible to request access (that will have to be manually reviewed by me). I've also dropped a couple of things related to user profile, etc., focusing instead on just getting the basics up and running.
Next big step is doing a dump of the Supabase database and writing a migration script so I can get off Mr. Bones' wild ride
Okay, so this is pretty cool. This follow-up is written on an instance no longer served by Vercel and Supabase, but instead running a Go binary backed by a SQLite database on my own VPS!
Today I've managed to do a bunch of stuff. First I finished up some styling issues with the rewrite. The old app used tailwind and postcss, so I had to rewrite that into plain CSS. There's still a few bits missing, but nothing on the public pages.
Next, I added a bunch of structured logging using the slog package. Makes for easier debugging, and let's me see if there's any activity hitting the application. I've also got the access logs from Caddy, but I think it's nice to get a bit more info on how the app handles it. I also added signal handling of SIGINT and SIGTERM so that the http server waits to finish up any ongoing requests before shutting down. Should make deployments way more graceful.
A big piece of the puzzle was migrating data off of Supabase. The old database was PostgreSQL, and used Supabase's row-level security for authorization. Luckily, I was the only one who ever wrote anything, so all posts belonged to me. That made the migration way easier, as I could just create a user in the new database, dump the threads and posts as JSON using row_to_json, and insert them all with the new user as the owner. I've also moved post and thread IDs from UUIDv4 to UUIDv7. Probably a premature optimization, but having them time-sortable on the primary key is a nice property in my opinion. That part was the most finicky, and required me to rip off some of the code from github.com/google/uuid and make it accept a time.Time and set the time-bits based on that (instead of current time).
Finally, time came to get it deployed. I created an Ansible role for the app that includes setting up a app-specific user and group with no shell, creating all the necessary directories with the correct permissions, cloning the app repository onto the ansible host (not the remote) and building the binary for the correct OS and architecture. This binary is then pushed to the remote, along with a systemd unit file, and it makes sure to start/restart the service as necessary. The final playbook also includes the Caddy role to make sure that the Caddyfile is updated to let Caddy reverse-proxy requests to the app.
With all that done, the only thing remaining was switching the DNS and all should be good, right? Wrong. As I had configured Caddy before changing DNS, Caddy had started making requests to LetsEncrypt to get a SSL certificate for micronotal.com. But the ACME challenge failed each time, as micronotal.com was still pointing to Vercel. As this certificate dance is done on each reload, and I was doing a bit of trial-and-error-debugging, I quickly hit the LetsEncrypt rate limit on failed authorization requests.
After changing the DNS and waiting an hour for the rate-limit window to open it was time to try again. And it failed, again. The error messages were a bit cryptic, so I went for the good ol' if at first it doesn't work, try again. And again. And a few more times. Maybe it works this time? No? Okay, one more time should do it.
Unsurprisingly, it didn't magically resolve itself. Went a bit back and forth, but managed to find the cuplrit when I check the record for micronotal.com using dig, and then attempted to curl said IP and getting a response that definitely wasn't from a service of mine. Turns out I left out a '6' in the DNS record ¯\_(ツ)_/¯
Got that fixed, waited 5 mins for the TTL, waited another 5 minutes for the LE rate-limiter, and now we're here!
I think I've actually completed the migration now. I've fixed most if not all of the remaining cosmetic issues, and the site is super snappy which is really fun. I just deleted the old projects on Vercel and Supabase. Feels good!