Running Umami analytics for free on Vercel and Supabase

ig0r01 pts0 comments

Running Umami analytics for free on Vercel and Supabase - Igor Kulman

I have never used Google Analytics on my personal websites. It always felt far too invasive and complicated for what I actually wanted to know: whether anyone reads my articles, which pages people visit, and where they came from.

For a while I used Simple Analytics. It was privacy-friendly and did exactly what I needed, but it was still somebody else’s analytics service. I wanted to try running something under my own control instead.

I found Hans-Jørgen Hjerpbakk’s article about self-hosting Umami, using Fly.io for the application, Supabase for PostgreSQL, and a Cloudflare Worker as a first-party proxy.

It was almost what I wanted, but not quite. Fly.io is no longer free for new accounts, and using the Cloudflare Worker would mean moving my DNS to Cloudflare. I wanted the whole setup to cost nothing and I did not want to move DNS just for analytics.

My websites were already hosted on Netlify, so I ended up with this instead:

Website on Netlify<br>↓ same-origin rewrite<br>Umami on Vercel<br>Supabase PostgreSQL

The result costs me nothing for the traffic my personal websites receive. Getting there was not difficult, but there were a few traps — especially one involving Supabase connection pooling that only appeared after a fresh Vercel deployment.

Deploying Umami without running a server

Umami is open source and has an official Vercel deployment guide. Vercel can create a private GitHub repository from the Umami source and deploy it as a Next.js application.

The application needs a PostgreSQL database, so I created a free Supabase project in the same general region as the Vercel deployment.

The important environment variables are:

DATABASE_URL<br>DIRECT_DATABASE_URL<br>APP_SECRET<br>DISABLE_TELEMETRY=1

APP_SECRET should be a random value. It can be generated locally with:

openssl rand -hex 32

The two database URLs look redundant, but they serve different purposes. This distinction turned out to be the most important part of the whole setup.

The Supabase connection pooling trap

Supabase offers multiple ways to connect to PostgreSQL. I initially used the session pooler on port 5432 for DATABASE_URL.

The first deployment worked. Umami created its tables, I could log in, and analytics events appeared in the dashboard. Nothing suggested the configuration was wrong.

Later I pushed another change, causing Vercel to perform a completely fresh deployment. The Umami homepage and heartbeat endpoint still worked, but login and every other database-backed request returned HTTP 500.

The Vercel logs contained the actual explanation:

EMAXCONNSESSION: max clients reached in session mode<br>max clients are limited to pool_size: 15

The session pooler reserves a connection for each client session. That is a poor match for serverless platforms where multiple function instances can appear and hold their own connections. The likely explanation was that the original deployment had used enough of the available pool that the fresh one exhausted the remaining slots.

The correct setup is to use Supabase&rsquo;s transaction pooler on port 6543 for normal application traffic:

DATABASE_URL=postgresql://...pooler.supabase.com:6543/postgres?pgbouncer=true

Database migrations need a non-transaction-pooled connection. Supabase&rsquo;s direct connection (db.YOUR-PROJECT.supabase.co:5432) is preferred when it is reachable; the session pooler (...pooler.supabase.com:5432) can be used from an IPv4-only environment instead. I used the session pooler:

DIRECT_DATABASE_URL=postgresql://...pooler.supabase.com:5432/postgres

Umami uses DATABASE_URL while the application is running and DIRECT_DATABASE_URL when executing prisma migrate deploy during a build.

After changing the runtime URL to port 6543, keeping the port 5432 session-pooler URL only for migrations, and redeploying, database access worked normally again. I also verified a clean production build with migrations enabled. There were no pending migrations, login returned a normal authentication response instead of HTTP 500, and the tracker continued accepting events.

During recovery I temporarily set SKIP_DB_MIGRATION=1. That can be useful to get a broken deployment online, but it must not remain enabled. Otherwise a future Umami update can deploy new application code without applying the corresponding database schema changes.

This was the deceptive part: the wrong pooler did not fail during setup. It worked until a later deployment changed the number of active connections.

Making analytics requests first-party with Netlify

The original article used a Cloudflare Worker to expose the Umami tracker under the website&rsquo;s own domain. Because my sites already run on Netlify, I could do the same thing with two rewrite rules and no additional service.

My Hugo sites use a _redirects file containing:

/np/x.js https://YOUR-UMAMI.vercel.app/script.js 200<br>/np/api/send https://YOUR-UMAMI.vercel.app/api/send...

umami supabase vercel pooler analytics deployment

Related Articles