Skip to content

How to run it

A Next.js app and a Postgres database.

A hosted version isn’t available, so running it means running it yourself. It’s meant to be small: nothing to operate beyond what runs on Vercel, or a folder and two scheduled calls anywhere else.

What it costs

Trailic has no licence fee, no seats and no credits. What you pay is whatever your host and your database charge you, and we don’t quote their prices here because they change.

  • On Vercel’s Hobby plan an hourly cron job won’t deploy, so the hourly flush in vercel.json needs Pro, or a schedule of your own.
  • Vercel Queues is on every plan and in beta: being in beta is the risk, and a small port in the code is the answer to it.
  • A serverless Postgres bills by the hour it’s awake. Events reach it in hourly batches and an empty buffer doesn’t wake it, so it can sleep in between.

Run it locally

You need Node 20 or later, pnpm and Postgres. Docker gives you one.

docker run -d --name trailic-db -e POSTGRES_PASSWORD=local -e POSTGRES_DB=trailic \
  -p 127.0.0.1:5432:5432 postgres:17-alpine
git clone https://github.com/trailic/trailic.git && cd trailic
pnpm install
cp .env.example .env.local   # then fill it in
pnpm db:migrate
pnpm dev

Signing in needs one of the ways the README lists. The quickest locally is TRAILIC_PASSWORD_SIGN_IN=true with RESEND_API_KEY set, and your address in TRAILIC_ADMINS. Events wait until the flush runs, so locally you call it yourself:

curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/flush

On Vercel

vercel.json deploys it with its functions in Frankfurt (fra1). Nothing else is needed for the buffer: Vercel Queues authenticates by the deployment’s own token. Apply migrations with pnpm db:migrate before starting a new version. The rate limit in the app counts per server instance, so for a hard limit against a flood, add a rule in Vercel’s firewall.

Elsewhere

  • Set TRAILIC_BUFFER=file and point TRAILIC_BUFFER_DIR at a disk that lasts, or write another buffer: it’s one small port.
  • Call GET /api/cron/flush hourly and GET /api/cron/retention daily, with Authorization: Bearer <CRON_SECRET>. With nothing to do they answer without touching the database.
  • What the collector and the jobs know about sites is kept with Next’s use cache: remote. Without a cache handler it lives in the server’s memory: fine for one server.
  • Put it behind a proxy that sets X-Real-IP or X-Forwarded-For, and X-Vercel-IP-Country if you want countries. Without a proxy that overwrites these, anyone can claim any address.

The environment you must set

TRAILIC_DATABASE_URL
Postgres. The role must be able to create tables: raw events get a new partition each month.
BETTER_AUTH_SECRET
At least 32 characters. Signs sessions: changing it signs everyone out.
BETTER_AUTH_URL
The instance’s public address. Snippets and assistants’ tokens are built from it.
TRAILIC_SECRET
At least 32 characters. Signs site keys and derives each day’s visitor salt.
TRAILIC_ADMINS
Addresses that may sign up and start workspaces. Anyone else joins by invitation.
CRON_SECRET
The bearer the jobs must be called with. Without it, they refuse everyone.

And one way to sign in: a company’s OpenID Connect provider, Google, or passwords. Every variable is in the README.

What the load test measured

Measured with pnpm load-test on Postgres 17 limited to one CPU and 4 GB: 5 million synthetic events over 120 days on 30 sites, the biggest with a quarter of the traffic. These are synthetic events on one machine, not real traffic, and you can run the same test yourself.

What the load test measuredOne CPUA quarter of a CPU
A flush of 4,500 batches4 s36 s
Overview, biggest site, 90 days70 ms200 ms
Overview, all sites, 90 days600 ms2.7 s
Funnel, three steps, biggest site, 90 days130 ms700 ms
Retention, 13 weeks150 ms700 ms
Journeys from the biggest site’s top page, 90 days3.5 s20 s

Raw events took 1.64 GB, about 340 bytes an event, and rollups 139 MB.