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.jsonneeds 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=fileand pointTRAILIC_BUFFER_DIRat a disk that lasts, or write another buffer: it’s one small port. - Call
GET /api/cron/flushhourly andGET /api/cron/retentiondaily, withAuthorization: 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-IPorX-Forwarded-For, andX-Vercel-IP-Countryif 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 measured | One CPU | A quarter of a CPU |
|---|---|---|
| A flush of 4,500 batches | 4 s | 36 s |
| Overview, biggest site, 90 days | 70 ms | 200 ms |
| Overview, all sites, 90 days | 600 ms | 2.7 s |
| Funnel, three steps, biggest site, 90 days | 130 ms | 700 ms |
| Retention, 13 weeks | 150 ms | 700 ms |
| Journeys from the biggest site’s top page, 90 days | 3.5 s | 20 s |
Raw events took 1.64 GB, about 340 bytes an event, and rollups 139 MB.