What you are actually running
Three data stores and four services. The split is not arbitrary: events are immutable and arrive fast, so they live in ClickHouse, and person profiles mutate constantly as merges, traits and interest scores land, so they live in Postgres. Redis sits between collection and storage.
That middle layer is the part worth understanding before you deploy. The collector's job is to answer the visitor's browser and stop. It publishes to a Redis stream and returns; a worker consumes the stream and writes to ClickHouse, acknowledging only after ClickHouse confirms. A slow, restarting or briefly unreachable database therefore never becomes a slow response on somebody else's website.
Browser ──▶ apps/ingest ──▶ Redis Stream ──▶ apps/worker ──┬──▶ ClickHouse (events)
(Bun + Hono) └──▶ Postgres (profiles)
p99 <10ms ▲
packages/queries ────────┘
│
apps/web (dashboard) · apps/mcp (assistants)Requirements
One machine that can hold Postgres 17, Redis 7 and ClickHouse 25.3. Falorb is built for small-to-medium traffic on modest hardware, and the ClickHouse configuration in the repository ships tuned for a shared host rather than a dedicated one: capped memory, capped pools and bounded system logs.
- Node 22 or newer, and pnpm
- Docker and Docker Compose
- Bun, if you want to run the collector the way the reference deployment does
- A domain you can point three subdomains at
Bring up the data stores
The compose file in the repository defines all three with the tuning already applied. Nothing else needs to exist yet.
git clone https://github.com/obhox/Falorb.git
cd Falorb
pnpm install
cp .env.example .env
docker compose -f infra/docker-compose.yml up -dSet the two secrets that matter
FALORB_SALT_SECRET is what the daily-rotating IP hash salt is derived from. It is the reason no raw address is ever stored, so it must be set before anything collects.
BETTER_AUTH_SECRET ships as a low-entropy placeholder that warns on every boot. It signs session cookies, so rotate it before the first real account exists rather than after, because rotating it signs everybody out.
openssl rand -base64 32 # FALORB_SALT_SECRET
openssl rand -base64 32 # BETTER_AUTH_SECRETMigrate both stores, then seed
Two runners, because the two stores are genuinely different. Drizzle handles Postgres. ClickHouse has a purpose-built runner that splits statements, substitutes placeholders and redacts credentials from any error it reports.
Sign up in the dashboard first, then re-run the seed with your own address. Without it the seeded properties belong to an organization your account is not a member of, and you land in an empty portfolio wondering what went wrong.
pnpm --filter @falorb/db migrate # Postgres
pnpm --filter @falorb/db ch:migrate # ClickHouse
pnpm --filter @falorb/tracker build
# after signing up in the dashboard
[email protected] pnpm --filter @falorb/db seedStart the services
The collector receives events and publishes. The worker consumes the stream, writes to ClickHouse, and runs the eleven scheduled jobs behind identity resolution, sessionization, path transitions, interest scoring, enrichment, alerts, GDPR requests and retention.
bun apps/ingest/src/index.ts # collector
pnpm --filter @falorb/worker start # worker + scheduler
pnpm --filter @falorb/web start # dashboardPut three hostnames in front
The collector, the dashboard and the MCP server go on separate subdomains. The collector is publicly reachable and high-volume, so an ad-blocker rule against it cannot take the dashboard down with it, and the two can be cached and scaled independently. Serving the collector from your own subdomain is also the practical answer to blocklists, which target vendor hostnames.
The repository ships a Caddyfile that terminates TLS and sets the cache headers, including a one-year immutable cache on the tracker script.
- a.yourdomain, the collector
- dashboard.yourdomain, the dashboard
- mcp.yourdomain, the MCP server, for assistants connecting remotely
Prove it works
Every claim in the repository has a command attached, and these are the ones worth running before you point real traffic at it. The load test is the important one: it asserts that every acknowledged event actually reached ClickHouse, which is the claim a collector is most tempted to fudge.
curl https://a.yourdomain/health # redis, geo, tracker readiness
node scripts/loadtest.mjs # every acked event landed
pnpm --filter @falorb/worker verify:jobs # all 11 scheduled jobs run
pnpm --filter @falorb/queries smoke # 32 queries on live ClickHouseKeep it running
Backups are two strategies because the stores behave differently: the event history is large and append-only, the control plane is small and constantly rewritten. The repository ships infra/backup.sh with incremental ClickHouse and a verified gzip dump for Postgres.
Upgrades are pull, install, run both migration runners, restart. Migrations are additive, and the historical backfill script assigns totals rather than incrementing them, so re-running it after a half-finished attempt is safe.
The usual questions
How much server does this need?
The reference deployment runs a portfolio of eight sites on a single small server alongside other services. That is what the capped ClickHouse configuration is for. It is not built to be a hyperscale event warehouse and does not pretend to be.
Can I skip ClickHouse and just use Postgres?
No. The query layer is thirty-two parameterized ClickHouse queries, and the rollups, skip indexes and monthly partitions the reports depend on are ClickHouse features. If one Postgres instance is all you want to operate, a lighter tool is the honest recommendation.
Does it work behind Coolify, Dokku or a platform proxy?
Yes. Dockerfiles and a production compose file are in the repository with a deployment guide in infra/DEPLOY.md. The apps set their own security headers rather than relying on the Caddyfile, so those follow the deployment onto any platform, including ones that terminate TLS at the edge and never load it.
Run it and see
The quickstart is five commands and needs no account anywhere.