Docs Start here

Web UI

Every screen in the browser, and — stated screen by screen — what each one does not do yet.

Open http://localhost:3000 (or your domain, if you installed with one) and sign in with the admin account — see Admin login for where that password comes from and how to change it. It is served on the same origin and port as ingest — there is no separate admin host or port to firewall off separately. Everything the Admin login section says about that origin being reachable from wherever ingest is reachable applies to the login form too.

A fresh install, signed in with no project yet, gets a first-run wizard instead of the normal screen. Name a project, and the wizard hands back the install snippet and the project’s one-time server key — shown once and never again, the same discipline Settings uses for every later project’s key, described below — then waits for a real first event to arrive. It never claims a working install on a timer, only on an event actually landing, and an arriving event does not dismiss the wizard by itself either: it flips the last step into a success state and waits for you to click “Continue to dashboard” — so the key stays on screen until you say you’re done with it. There is also a “Skip to dashboard” for the case where you cannot instrument the target site right now.

Past the wizard (or immediately, if a project already exists), there are eight screens, reachable from the sidebar:

  • Dashboards — several saved reports on one screen: a named, ordered grid of tiles, each a saved trend, a saved retention report or a funnel, half or full width. One range picker applies to every tile and lives in the URL, not in the dashboard, for the same reason a saved report never stores its range. Its default setting is the exception, and the screen says so: at Default for this resolution no range is sent at all, so each tile falls back to its own report’s default window and the tiles are not on one period. Pick a preset to put them on the same range. A project can have many dashboards; the star — on a list row, and on the dashboard itself in both view and edit mode — marks one as home. It opens at / after login, and “Dashboards” in the sidebar and the Lyraflow mark both go to it when one is starred (the list otherwise); “All dashboards” on a dashboard opens the list. Clicking the filled star clears it, leaving the project with no home. Editing happens in place — rename, reorder, resize, add, remove — and every change saves as it is made. A tile whose report has since been deleted stays on the dashboard and says so rather than vanishing. Clicking a tile opens the report it shows, over the dashboard’s range — for a funnel only when that range is one the funnel screen itself offers (24 hours, 7, 30 or 90 days), and otherwise the funnel opens on its own default and says which range it ran. At most twelve tiles, and at most three of them run at once, so opening a dashboard does not fan a dozen queries at ClickHouse together. A tile whose stored definition would exceed a server ceiling under the current range — too many points, too many cohorts, or a funnel range past 90 days — warns instead of running, the same way its own screen does. A dashboard can be shared by a secret link from its Share button, in view mode; see Sharing a dashboard. What a dashboard does not do yet: it does not refresh on its own, and a tile is always a saved report — no single-number tiles, no text.

  • Feed — a live event feed, split into an Accepted tab and a Rejected tab, over a window you pick — the last hour through the last 90 days — with an optional event-name filter. The window and the filter are held in the URL, so a refresh keeps them and the screen can be shared as a link; the page polls every few seconds on the short windows and once a minute on the long ones. The chart above the tables counts events per bucket over the same window, at the finest resolution that window allows. The event filter reaches the chart and the Accepted tab but not the Rejected one: a payload may have been refused precisely because its event name was missing or unparseable, so filtering the rejections by name would hide the rows that tab exists for. Rejected events carry the reason they were dropped next to each row — validation_failed, too_many_properties, event_name_cardinality or property_key_cardinality — which is otherwise only visible by reading server logs. An unauthenticated or over-quota request is refused before it reaches a project at all, so it is never dead-lettered and never shows up here — the Rejected tab tells you about payloads that reached a real project and were still refused, not about a bad or missing write key.

  • Settings — the install snippet for the active project (so losing the copy from the wizard is not a trip to the CLI); the project list, where each one can be renamed or archived; this month’s usage (accepted, rejected, throttled, and the quota — reading plainly as Unlimited rather than a bar or a number when none is set); the project’s retention and monthly quota, both editable in place; and the full project list with a create-project flow of its own, whose server key is likewise shown exactly once and never again.

  • Funnels — create a funnel from an ordered list of events, run it over a range you choose, and read the result as one row per step: how many people reached it, what share of the entrants that is, and how many dropped between it and the step before. Opening a saved funnel runs it once; changing the range does not re-run it — the chart dims and waits for you, because a funnel is a real scan and because numbers from the old range sitting under a new one would be a wrong answer stated confidently.

    Two honesty details worth knowing, both of which the screen states without being asked. If some of the people who entered did so too recently to have had the funnel’s full window, it says so and tells you how many — otherwise every run over a range shorter than the window quietly under-reports conversion. And if a funnel’s segment filter has been deleted, the run succeeds over everyone rather than failing; the screen reports that and stops showing the filter as though it applied, because the numbers alone look entirely normal.

    Click a step and a Reached/Dropped panel opens beneath the chart — two different populations, each counted on its own rather than assumed from the chart above (see Who reached a step, or stopped there under Funnels below).

  • Trends — how many of an event over time, optionally split by an event column or by a key from its own properties. It runs on demand rather than on render, and it can be saved: create one, give it a name, and reopen it later from the Trends list, the same as a funnel. What is saved is the event, the interval and the breakdown — not the range, so reopening a trend runs it over whatever range the screen currently has, never the one it was saved with. A split is drawn as small multiples — one panel per value, all on one shared scale, which the screen states — rather than as overlaid coloured lines: Lyraflow’s palette is a single copper ramp built for ordinal data like funnel stages, and a breakdown’s values have no order, so there is no honest colour to give them. Ten panels at most; everything smaller is summed into (other), and the screen says how many values that was so the panels still add up. Every point is marked, and hovering one reads out its bucket and its value in every panel at once — the pointer picks a moment and each series says what it was doing then.

  • Retention — pick a start event, a return event, a condition on either of them, a period and how many of them, and run a cohort grid. It does not run on render and does not re-run when you change the controls: a grid is a real scan, and numbers from one definition sitting under the controls of another is a wrong answer stated confidently, so the grid clears and waits for you. Cells shade by retention relative to the strongest cell in that grid, which the screen states — a narrowed grid peaking at 15% would otherwise render with no visible colour at all, so shades compare within one grid and never between two. A period that had not finished when the grid ran shows a dash rather than 0% — with a line underneath saying how many did, because a dash read as a zero is the one way this chart misleads.

    It can be saved too: create one, name it, and reopen it later from the Retention list. What is saved is the two events, their conditions, the granularity, the period count and the segment — not the range, so reopening a report runs it over whatever range the screen currently has. Two things can stop that run before it starts. A range and granularity that would together exceed 60 cohorts disables Run and computes nothing — the same ceiling and warning a fresh grid gets. A report whose stored filters no longer parse says so and skips the automatic run too, but leaves Run enabled, so the operator can still run the degraded version knowingly rather than being locked out of it.

  • Segments — build a filter tree in the browser: and/or groups, traits, context, lifecycle bounds, and behaviours with their own where predicates. Preview it before saving — the person count and a bounded page of members, taken at one instant — then save, re-run, edit or delete it. Clicking a person opens what the preview already knows about them: their latest country, city, device, OS and browser, the referrer and campaign they arrived through, and the traits identify() has set. Attributes with no value are left out and counted rather than listed as blanks, and a person carrying more traits than a row returns says how many are not shown.

    The same honesty details as Funnels, for the same reasons. 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; a segment that has never been evaluated says so rather than rendering as a count of zero. The list does not silently re-evaluate everything on every visit, because each evaluation is a real ClickHouse scan. And a segment whose stored tree no longer parses opens read-only rather than being offered for editing as though the builder understood it.

  • People — a person profile at /people?id=…: the canonical id and every id ever bound to it, split into user ids and device ids; first seen, last seen and the event count; their traits; their latest context — device, browser, OS, country, referrer and campaign — read from their newest event; and their full event timeline, newest first, paged backwards a page at a time and bounded by their own first-seen and last-seen rather than by the last 24 hours — so a customer last active in June opens on June, not on an empty screen. Reached from a segment member row, a funnel step’s people panel, the feed’s person cell, the sidebar, or either of the two searches on the screen itself.

    Those two sit side by side whenever no id is in the URL. The lookup box takes an id you already have. Find by trait is for when you have none: one condition on one named trait, built exactly as a segment’s trait condition is and offering the same operators — compare, text (contains, starts with, ends with, and their negations), presence, true or false, relative date — with the value box that operator calls for, or none at all for is set and is true. It runs through the same engine a segment preview uses, listing everyone who matches with their traits and a link to each profile, paged the way a segment’s member list is. One condition is the shape of the search, not a claim that the matching is exact: what it will not do is combine several conditions — that is what a segment is for — look for a value across every trait at once, since you name the trait, or list everyone without naming a condition at all. The condition lives in the URL, so a search that matched nobody is still a link that says so after a reload.

    The entry points reach further than the profile does. A person only enters the identity graph through identify(), so a visitor who has only ever been tracked anonymously has events — visible in the feed, under their anonymous_id — and no profile to open. The feed still links their rows, and the profile answers by naming that as the first of the four things a “nothing to show” can mean, rather than by hiding the link or by claiming the id was never seen. On a fresh install, where little has been identified yet, that is the common case. It is the same limit Privacy: deletion and export below documents for the API, met from the UI side. The rows that do open a profile carry a person icon — in the feed’s accepted table and in every member list — so which link pays off is visible before the click.

    Two privacy actions sit on the profile. Export buffers the subject-access response in the browser and triggers a save — past 50,000 events it shows the equivalent lyraflow persons export command instead, because a button that starts a download doomed to hang is worse than no button. Delete is the same two-step, typed-id-confirm pattern project deletion uses, then polls to completion rather than trusting the request that started it.

The account menu in the header also has a Profile screen, for changing the admin’s email address and password, and for picking the UI’s accent colour. Seven palettes — copper, the default, plus cobalt, moss, plum, slate, wine and amber — each the same ramp with the hue swapped, and each measured for contrast in both light and dark before it shipped (brand/contrast-report.txt). The choice is kept in the browser, not on the server: it follows the browser, not the account, and changes the accent only — surfaces, text and the status colours are the same in every palette. Both account changes require the current password — a session is enough to read everything this install holds and deliberately not enough to change what recovers the account — and a password change signs out every other browser, which is the point of changing it after a leak. There is no confirmation email, because Lyraflow sends no mail; a new address takes effect immediately.

Archiving a project stops Lyraflow accepting events for it and nothing else. Its data is untouched, every report still works, retention still applies, and restoring it is one click. Events sent while it is archived are refused, not queued, so they do not arrive later. Renaming never changes the slug — the slug is what lyraflow commands address a project by, so a rename would otherwise break scripts silently.

Deleting a project destroys it. Every event, person, trait and report, in both databases, plus the project row itself — and unlike archiving there is no way back short of a backup. Deleting asks you to type the project’s slug, from Settings and from the CLI alike, because nothing else about the action is reversible.

It runs as a background job rather than a single request: ClickHouse holds a project’s events across partitions in three tables and two more that need asynchronous mutations, which takes minutes on a large project. Confirming stops Lyraflow accepting events for the project, then the teardown waits for the last in-flight events to drain before it starts — about a minute on a default install, because each project’s row is cached in memory for that long and a teardown that raced it would drop partitions those events were still landing in. Then it tears ClickHouse down, confirms nothing is left, and only then removes the project from Postgres — in that order, so a half-finished delete can be retried rather than leaving data nothing will ever sweep again. Settings shows the progress; lyraflow projects deletion get <id> reports the same thing, and lyraflow projects deletion retry <id> resumes one that gave up.

Volunteering the limit: that is the whole UI. A person profile exists, but there is no People list — nothing browses or searches persons, so the lookup box above only helps when you already hold an id, and only when that id belongs to someone identify() has bound. Reach anyone else the way the rest of this document shows, over the HTTP API or the CLI.

The funnel screen’s per-step people panel is backed by POST /v1/funnels/:id/people (see Funnels below) — the same bounded member list Segments uses, traits and all. It opens a person profile from there now, same as Segments and the Feed do. The CLI has not caught up to this yet: lyraflow funnels dropoff still only walks the dropped population, so reading who reached a step is UI- and API-only for now.

This page is the Web UI section of the product README at v0.15.0. It is generated from that file rather than written here, so a correction belongs upstream.