Self-hosted customer journey analytics.
Lyraflow records what people do in your product, stitches anonymous visits to known accounts, and lets you ask who did what.
It runs on your own machine under Docker, and nothing leaves it.
About five minutes
Four steps to a running install, most of it waiting for Docker.
Two containers
Events go to ClickHouse and identity to Postgres. Old events expire on a schedule you set.
Every limit has a number
It is written down, in the documentation, and the ones with known slack say so.
Knowing who someone is
A visitor browses anonymously, signs up two weeks later, then uses your product from a phone. Lyraflow ties all of that to one person, and reads their history back stitched.
If two people share a device, each event is attributed to whoever was signed in at that moment — not to whoever used it last.
Asking who did what
Build a filter tree in the browser — traits, context, lifecycle
bounds, and behaviours with predicates of their own — and read it
back in plain English before you trust it. Clicking a person shows
what is known about them: where they are, what they arrived
through, and the traits identify() has set.
A saved segment's count is the server's cache from its last evaluation, shown with the instant it was taken and never passed off as current. Each run is a real scan, so nothing re-evaluates itself just because you opened a list.
Where people stop
A funnel is a list of events in order, over a window you choose: how many people reached each step, and how many did not go on. It is drawn as a flow rather than a bar chart, so the people who leave are a band that peels away and stays visible instead of a gap you have to infer.
A step can be marked optional, and the people who skipped it still count at every step after it — without that, one detour through a help page reads as a drop-off. The window is how long someone has to finish, and it is part of the funnel rather than part of the question you ask this time.
Whether they came back
Of the people who did one thing in a period, how many came back and did another in the periods after it. Both events are yours to pick, and each takes a condition — so on a site where every navigation is one event name, “viewed the home page, then came back and registered” is a question you can actually ask.
A period that had not finished when the grid ran shows a dash, never a zero, and the screen says how many cells are waiting. Reporting an unfinished week as nought draws a collapse in the newest cohorts, in exactly the corner a reader scans for a trend.
How a number splits
An event over time, broken down by a property it carries or by a column of the event itself. Events with no value there are kept as their own series rather than dropped, so the parts still add up to the whole and a number here can be checked against the feed.
Each value gets its own panel, and every panel is drawn on one shared scale — so a series ten times another’s size looks it. Ten at most; anything smaller is summed into one labelled series and the screen says how many values that was.
The event can be narrowed before it is split, with the same conditions a funnel step and a retention grid take. Without that, a site whose every navigation is one event name can only chart “a page was viewed”; with it, the registration page, by referrer is one chart. A half-written condition blocks the run rather than being quietly dropped, because a filter that silently disappears answers a wider question than the one on screen.
A report you can keep
A retention grid or a trend can be named and saved, the same way a funnel is, and reopened from a list that shows what each one asks rather than only what it is called.
What is saved is the question, never the window. “Signups by day, last week” and “signups by day, this month” are one report asked twice. The consequence is deliberate and visible: reopen a saved report into a range it cannot answer — a minute-by-minute chart across a year — and the screen says so and refuses, rather than quietly widening the definition you saved to fit the window you are in.
curl https://analytics.example.com/v1/trends \
-H "x-lyraflow-server-key: $LYRAFLOW_SERVER_KEY" Several reports, one screen
A dashboard is an ordered grid of tiles, and every tile is a report you already saved — a trend, a retention report or a funnel — at half width or full. A project can have many, and starring one makes it home: home is what you land on after signing in, and what the sidebar opens from anywhere else.
The range belongs to the page, not to the dashboard. One picker at the top governs every tile and lives in the URL, so what you are looking at is what you can send someone. Its default setting is the exception: no range is sent at all, each tile falls back to its own report’s window, and the tiles are then not on one period — which the screen says under the picker rather than leaving you to assume.
A dashboard can be handed to someone without an account. Share mints a secret link, after saying what the link exposes; whoever holds it opens the dashboard, picks a preset range and sees every tile, and can ask for nothing the dashboard does not already show. Revoke it and the next request is a 404.
What it does not do yet: nothing refreshes on its own, a tile is always a saved report — no single-number tiles and no text — and a link has no password, no expiry and cannot be embedded in another page.
curl -X POST https://analytics.example.com/v1/dashboards \
-H "x-lyraflow-server-key: $LYRAFLOW_SERVER_KEY" \
-d '{ "name": "Growth overview", "tiles": [
{ "kind": "funnel", "report_id": 1, "width": "full" },
{ "kind": "trend", "report_id": 3, "width": "half" } ] }' A link, not an account
The same dashboard, as the person you sent it to sees it: no sidebar, no login, the range picker with its presets, and nothing on the page that leads anywhere else. The link is the only credential, it names this one dashboard, and the server builds every query from what is saved rather than from what the page asks — so whoever holds it can look, and only look.
Anonymous readers are bounded: 120 requests a minute per link, three tiles running at once, and a result kept for a minute so fifty people opening one link cost one run per tile. Revoke the link and every copy of it stops on the next request.
One person, end to end
Every id ever bound to someone — the ids you sent and the devices they were seen on — with when they were first and last seen, what they have done, and the traits they carry. Reachable from a feed row, from a segment’s member list, and by searching when you have no id in hand at all.
It reads the same stitched identity every other screen reads, so a person who looks like two people here looks like two people in a funnel as well. That is the point of showing it: identity problems are much easier to see on one person than in an aggregate.
Deleting someone completely
Deleting a person erases the underlying rows, and every read path — segment counts, member lists, person reads, exports — is filtered from the instant the request is accepted, not when the purge finishes. A person deleted a second ago is already invisible.
curl -X DELETE https://analytics.example.com/v1/persons/user-42 \
-H "x-lyraflow-server-key: $LYRAFLOW_SERVER_KEY" Try it
Four steps. About five minutes, most of it waiting for Docker. You need Docker and Docker Compose v2.21.0 or newer. Nothing else.
git clone https://github.com/lyraflow/lyraflow.git
cd lyraflow
./install.sh
That generates passwords into .env, starts three
containers, and waits until the app answers on port 3000. Pass a
domain name instead and it serves HTTPS itself.
Hear about new versions
What shipped, what changed, and what is still missing. Only when there is a release — which, at this stage, is not often.
One address, kept for release notes and nothing else. Unsubscribe from any of them.
Read the source
Every claim on this page is in the README, with the numbers attached.