Published October 27, 2023
Saving $400/month on Vercel Analytics by using Edge Runtime and Planetscale's free tier instead
A glimpse of the analytics table we'll be building
Vercel Analytics' free tier gives you 2,500 events a month, which isn't a lot. Its most efficient tier costs $20 per 500k events before you have to pick the phone and call for a better price. But you can set up an endpoint hosted on Vercel using the new Edge Runtime to get half-a-million invocations per-month for free and use that endpoint to write up to 10 million free analytics events per-month using Planetscale (I'm not affiliated in any way). The next 40 million events will cost you $29.
You can of course use any database you like but Planetscale is the cheapest managed solution. We're also going to be using Drizzle ORM in this tutorial because it makes the code simpler, safer, compiles down to regular SQL and if you want to bring your own database all you' have to do is delete a line of code. We'll also be using Vercel KV to do rate limiting.
The geolocation data Vercel provides is impressively precise, accurate to within 50 feet in some cases. I had to fake the geolocation data for this tutorial to avoid giving out the exact building I live in. Quite scary!
Just give me the code
Here's the code for a Next.js Route Handler that will read the IP addres and geolocation of a request and save it to a database. It handles rate limiting IP's per unique event type using a 5 second sliding window and validating the request body against a list of known events.
if you use this code in your project don't forget to modify your next.config.js to protect this route with CORS so it doesn't get spammed from non-visitors. You also have the option to wall off recording events to authenticated-users-only by reading, e.g. a session token from the request using the cookie function exported from
next/serverand mapping the session token to a user. The route handler above does not do that and treats events as anonymous.
If you want to know more about how to set up the full tech stack used in that example, including Drizzle ORM and Planetscale to get 10 million free events a month, read on.
If you're adding analytics to a project using this exact tech stack already I expect it'll take just a few minutes to get this set up. If you're starting a project fresh, this entire tutorial will likely take just 30 minutes.
The high-level components
- A table for our analytics events
- An edge function deployed on Vercel that collects the user's IP and Geolocation data and writes the data to the table
- A lil' frontend app that pings this edge function from the user's browser.
Each of these components can be deployed independently but the example code will use a single Next.js app using the new App Directory to manage the database, the API, and the frontend.
To get this production ready wee'll also need to rate limit events per user and per event type and batch events on the client so that we minimize the total number of edge function invocations.
1. Install bun
If you'd rather use npm or yarn you can skip this. Vercel supports Bun now so may as well to speed up deployments and local development. You can use it alongside npm when Bun has some gap in feature parity.
Follow the instructions at https://bun.sh/docs/installation to install it.
It also helps to have the Vercel CLI to manage environment variables automatically, and you can install it with
But you can choose to copy paste environment variables manually if you prefer.
2. Create the Next.js app
Run the below command from your command line to create a Next.js app using the App Router, Bun, and the
Now let's install the dependencies we'll need for the first iteration of our analytics.
@vercel/edge includes the utilities to pull the ip and geolocation from requests.
drizzle-kit wil let us perform migrations and push migrations to the connected database
drizzle-orm will allow us to write typesafe queries and take some boilerpalte out of the picture. It compiles down to SQL so there's no runtime cost to using it.
Zod will allow us to get some type safety on the server and discard invalid requests.
@planetscale/database exports a function that allows drizzle to create a connection to the Planetscale database, it's a set and forget config thing.
3. Set up the DB
Ok this section is a lot of boring hooking stuff up and copying around credentials but the good thing is you only have to do it once. Managing environment variables is the most tedious part of programming.
To get started create an account on Planetscale and create a table with a branch name of "dev". We'll connect to the dev branch for local development and the main branch for production.
Then, from the overview tab, click connect on the top right and then "new password" on the top right of the modal that pops up. This will give you a connection string that includes the username, password, branch, and URL of the database. It's the only credential we need to connect to the DB from our app. Do this once for the "main" branch and once for the "dev" branch and make sure to copy the DATABASE_URL string for both as you won't be able to see it after creation. Next step is to copy these into Vercel
(Skipping past setting up a project in Vercel, using git, and pushing to Github...) Navigate to the Environment Variables section in your Vercel project's settings, Uncheck "Preview" and "Development" and paste in the `DATABASE_URL="..."`environment variable using the credentials for the "main" branch of your Planetscale Database into the text fields and hit save. Do the same for the "dev" branch but uncheck "Production" and "Preview" before hitting save.
Now from a terminal somewhere in your project run the following commands to pull in the development environment variables into your local filesystem.
Next, we'll start writing some code. First make files we'll put our core DB code in.
In src/lib/db/db.ts we'll put the core code to initialize our DB and export the db connection to the rest of the codebase.
In the above codeblock we make sure we have DATABASE_URL set, and ensure it throws at build time if it's not. We also set up logging while in development mode but you can disable that entirely or even enable it in production. We export default a config that's read by Drizzle Kit so that it knows where to find our schemas to generate migrations and push DB changes.
Next, let's set up the schema for our analytics table:
To be honest, I haven't found "flagEmoji" to be a particularly useful column as it seems redundant with the country column, but I include it for exhaustiveness. Feel free to remove it for your project.
The API for our analytics event is that events have an "event_type" and "metadata". We can enforce the presence or lack of metadata for some events as the API layer and get some type-safety at build time with TypeScript.
Feel free to play around with the character lengths of these columns. I'm using 50 for the event_type column as I plan to stuff as much information into a structured event_type string as possible, but if you prefer a different approach you can get away with a smaller character allocation.