Recently, we’ve migrated our SaaS platform to run on the edge, using Cloudflare Pages. Before that, we had a single serverless function running on Vercel that after cold starting, remained awake for 15 minutes after the last request. The biggest reason we decided to migrate to edge functions was the much faster response times and essentially no cold start latency. Vercel also offers an edge hosting product, but it is a bit more pricey than Cloudflare’s, so we decided to move away from them.
However, moving to Cloudflare Pages proved to be more challenging than what we first thought.
Differences in the JavaScript runtime
While previously our serverless function ran on top of Node, now this was not the case anymore. Pages uses a different runtime that has no access to Node (it does have a Node compatibility flag, however for our dependencies this was still not enough, since Node imports must begin with node: to work). For us, this meant no bcrypt for hashing passwords anymore. Luckily, bcryptjs is a thing and it was pretty much a drop-in replacement that worked without Node.
ORM Replacement
Another issue we had to deal with was Prisma, a fantastic ORM that brings an amazing developer experience with it. However, we weren’t satisfied with the current state of Prisma and the edge. To use it, our only ”feasible“ option was to use their Accelerate beta product - a global edge cache that comes with connection pooling out of the box. Accelerate is free while in beta, but will likely be a paid product in the future. This convinced us to move away from Prisma and choose another solution to talk to our database in the edge.
We had Drizzle ORM and Kysely - a SQL query builder - as candidates for the Prisma replacement. While both options were attractive, we ended up choosing Kysely with their PlanetScale dialect. Since we’re moving to the edge for its speed, why not bite the bullet and ditch ORMs and their overhead altogether?
So this was the plan, and we had a lot of work to do.
Beginning the migration
The Kysely community created prisma-kysely, a code generator that translates our existing Prisma schema to corresponding Kysely types. This package made our lives easier, because we can still rely on Prisma to manage our database schema, while using Kysely to build our SQL queries that return fully typed objects.
We had 50+ Prisma queries across the application that had to be converted to Kysely. Little did we know that using a SQL query builder instead of a full fledged ORM was going to be an incredible learning opportunity.
Instead of learning the syntax of an ORM, we were now diving deep into MySQL documentation. Working directly with SQL enables us to learn much more than through an ORM - they almost always hide the tougher parts from the developer. One example is that we had to re-implement our cursor pagination logic for an infinite scroll page we have. Prisma made this super easy, while Kysely challenged us to dig deeper and really understand what was going on.
Using Kysely also brought to light improvements we could do to some parts of our system. In Prisma, we felt we were likely to do more SELECT (*)
rather than in Kysely. Each SELECT
had to be more explicit now. We also moved away from UUIDs - easy to do in Prisma, just slap a @default(uuid())
in the id field of your model and you’re done. However, Kysely with MySQL does not natively have this convenience, we have to handle our IDs ourselves or stick with AUTO_INCREMENT
. Since we’re changing things up anyways, we decided to use ULIDs - lexicographically sortable unique identifiers - which our database seems to love because of no more unnecessary re-indexing each time a new item is inserted.
Finishing up
After updating each Prisma query we had, we were now up and running in Cloudflare Pages (not after opening a new PR with more fixes). It. Was. Fast. Really, not even close to what we had before.
We’re now considering Turso, an edge-first database using SQLite to move our data to the edge as well, but that will have to wait! Our database is not on the edge yet, it’s a MySQL instance in São Paulo, Brazil.