In an effort to better learn Go and its built in concurrency features, I started up a personal project where I’d download the raw text files for Fantasy Football tiers from https://www.borischen.co/. Of course, go and its mutexs and waitgroups are way overkill for downloading 36 tiny text files, but the point was to learn on something low stakes (and fun!). That project eventually spun out to me learning a new technology called pocketbase (or baas — backed as a service). You can see the results of that project here: https://fftiers.israelimru.com.
Pocketbase Was Perfect
Though pocketbase has been perfect for what I need, I recently learned about Convex which handles a similar role to pocketbase with some key differences. But before I get into that, let me first describe why pocketbase was the perfect choice (at the time) for my fftiers project.
- it produces a single static binary, which made deploying on my own server extremely simple
- it’s sqlite under the hood, something I was already familiar with
- it can be extended in either Go or JavaScript, and ships with an official JS client SDK for the frontend, which was the exact tech-stack I was already working with.
And now, back to convex. Though pocketbase had already slotted in nicely with my project, if I wanted to grow the project outside of personal use and allow for saved users to save their various teams in the app, it wouldn’t be quite so simple. That, plus it’s always interesting to learn new technologies, especially when it seemingly comes out of nowhere.
Porting Pocketbase App to Convex
So, to start, I thought it’s probably best to simply port over my existing pocketbase backend into using Convex and plan how I may grow from there. To do so, let’s first describe the current project. I have the go fetcher which simply downloads the raw text of files from Boris’s site. Then, it converts all of that data into a JSON file which is then fed into a JS script to ingest into pocketbase’s database. From there, there’s a tanstack frontend which displays the data into a table which the user can manipiulate as they see fit. I also recently threw in the concept of saving your various fantasy rosters into browser local storage (there’s no concept of auth or saved users yet, so to export your team to another browser/device, you need to send it as a URL payload within the app — I was proud of that one).
So what were the pain points here? Well, since it started off as a simple Go fetcher, it’s not the cleanest archiecture; there’s a bash script to run the go binaries to poll and fetch new data, a JS script to upsert new information, docker to deploy, etc. If I had built this as a new project, I’d ideally want all of this done together. Enter Convex (finally!).
Converting the Go Fetcher
Where Convex seems to shine is through its use of typescript functions as a backend. So instead of having my scattershot approach to the entire app (bash, go, cron, tanstack), I could instead use convex & TypeScript for everything. So, first up was _convex/fetch.ts
Converting Cronjob
Instead of relying on the host machine to set up a cronjob to periodically poll for new tiers from our sacred Mr Boris, we can use — you guessed it — TypeScript for that! In _convext/crons.ts we poll for new files, checking the etag cache for new data, and properly sorting the contenr into the correct NFL “week”.
Schema
In my original project, the schema was built through the pocketbase admin dashboard (I would create/edit the tables through the UI hosted at /_/). And though Convex does offer up a dashboard of sorts for its properitary database, it doesn’t offer up this functionality (from my understanding, it’s more of a document database than a relational one). But in any case, the structure remains the same: we use _convex/schema.ts to define our schema for our tiers and scoring formats, etc.
Ingesting
Ingesting went from JS to… well it’s still JavaScript/TypeScript, but now instead of calling the running Pocketbase db and using an admin password and writing a bunch of different transactions — all through HTTP mind you — everything is done as a single transaction, meaning all rows are deleted and re-inserted with the latest data on every run, meaning less errors and no strange in-between states.
TanStack Query
Even on the frontend, we had to change up how we queried our data. Part of what made pocketbase so easy to work with was that it exposed its tables with an easy to use web api (normally I would have had to spin up an API Server from the Go side and serve up the data with custom SQL queries). But on the Convex side, we use their useQuery for React apps and we write the queries in _convex/ranking.ts. So far, we have replaced essentially all of our backend from disparate services to simply using TypeScript, which honestly is a huge relief because context switching can be difficult at times and I found myself legitimately limiting myself to just one side of the project on days where my brain was fried.
One note I’d like to bring up here though is that my Fantasy Football Tiers app is not quite the perfect fit for Convex because I’m leaving a very important feature on the table: live updates. One of the best features of convex’s in-house database and query library is that it’s automatically using websockets, meaning that updates are LIVE, without ANY work from the devloper. So even though my current app updates very infrequently (by design), if I were to rebuild this app into, say, a live Fantasy tracker, Convex would easily come out on top because any changes to the backend would instantly be shown on the frontend to the user — cool!
Moving Forward
I’ve alluded to it already, but I’m leaving some of Convex’s best features on the cutting room floor with my current implementation. And though it’s currently doing something I already had up and running, I like it enough that I will continue on using this over Pocketbase. Here are my next steps:
- Implement users and log in to save your rosters across devices
- Tier-change alerts: when the hourly update lands, use Convex’s scheduler to diff the new week against each user’s roster and surface which of their players moved up or down a tier