Docs Sending data in

Tracking more than one site

When to use one project and when to use several, and the CLI that manages them.

Two separate websites, a marketing site and the app behind it, staging and production: each of those is a project, and one install carries all of them. Run create-project once per site.

docker compose exec lyraflow node packages/cli/dist/index.js create-project "Acme Store"
docker compose exec lyraflow node packages/cli/dist/index.js create-project "Acme Docs"

Each call prints its own write key and server key. Names must be unique after slugification (Acme Storeacme-store), and running it twice with the same name is refused with a message saying so rather than a database error.

The keys are the project selector

No request to Lyraflow ever names a project. There is no project id in any path, no ?project= parameter, and no --project flag on the CLI. The key you present is the selection:

Write key wk_…Picks the project on ingest. The snippet on acme.example carries that project’s write key; the snippet on docs.example carries the other.
Server key sk_…Picks the project on every read, export and deletion. One lyraflow stats call reports on exactly one project — whichever key it authenticated with.

So switching projects means switching keys, and nothing else:

LYRAFLOW_SERVER_KEY=$STORE_KEY lyraflow stats --since 24h
LYRAFLOW_SERVER_KEY=$DOCS_KEY  lyraflow stats --since 24h

lyraflow snippet follows the same rule — it prints the install block for the project whose server key it authenticated with, so run it once per project and paste each result on its own site.

What a project separates, and what it does not

Separation is in the storage layout, not a filter applied at query time. The events table is partitioned and ordered by project first, and every table in Postgres — identity bindings, aliases, segments, saved views, ingest counters, deletion requests — carries a project foreign key. A segment id or person id belonging to another project answers 404, never 403.

Per projectShared by the whole install
Events, people, and identityThe Postgres and ClickHouse containers
Segments and saved viewsThe ingest buffer (see below)
retention_months — 13 by defaultThe retention worker’s schedule
monthly_event_quota — unlimited by defaultBackups: one script dumps every project together
Usage counters, and the quota 429LYRAFLOW_ALLOWED_ORIGINS (see below)

Three of those are worth stating plainly rather than leaving to be discovered:

LYRAFLOW_ALLOWED_ORIGINS is one list for the whole install. It is a server env var, not a project column — on the Compose stack, a line in .env that the lyraflow service’s environment: block passes through. Unset — the default — every origin is allowed and a second site needs nothing. But if you have set it, every domain you instrument must appear in that one list (https://acme.example,https://docs.example), because creating a project does not extend it. The symptom of forgetting is a site whose events never arrive while its snippet looks perfectly correct: the browser’s CORS preflight is refused before any request reaches ingest, so nothing is rejected, dead-lettered or counted anywhere you would think to look. Because that silence looks the same as a variable that never reached the server at all, the boot log states which of the two you have — see When the allowlist does not take effect.

The ingest buffer is one buffer, not one per project. It holds 100,000 rows by default (LYRAFLOW_BUFFER_MAX_ROWS), and it is shared. A burst on your busiest site can push the buffer to its limit and cause events from a quiet one to be throttled. A per-project quota bounds how much a project may accept in a month; it does not reserve capacity for it in the moment.

A backup is per install. backup.sh and restore.sh operate on both databases whole. There is no way to back up, restore, or move one project on its own.

Identity does not cross projects

This is the consequence most likely to be discovered late, so decide it before you instrument anything.

Identity bindings and person aliases are keyed by project. The same user_id in two projects is two unrelated people. Calling identify('user-42') on both of your sites produces two separate profiles, with separate event histories, that no query joins and no merge can combine. A person who signs up on one site and later reads the other is two visitors, and Lyraflow will never tell you they are the same human — not because the join fails, but because it is never attempted.

That is the right model when the sites are genuinely separate products. It is the wrong one if you want to answer “did the docs visit lead to the signup?”

If you want one journey across both sites

Use one project, and put the site on every event as a property:

lyraflow.init({ writeKey: 'wk_...', host: 'https://analytics.example.com' })
lyraflow.track('signup', { site: 'store' })

Identity then works across both — one user_id is one person, and their journey spans the sites — and segments filter on site like any other property. What you give up is everything in the “per project” column above: one retention setting, one quota, one server key that reads both sites, and a site filter you must remember on every query, since forgetting it silently returns both.

Neither choice can be changed later without re-ingesting, because it decides how identity was resolved at write time. Separate products: separate projects. One product across several domains: one project.

lyraflow projects

lyraflow projects list
lyraflow projects delete <slug> [--yes] [--queue]
lyraflow projects deletion get <id>
lyraflow projects deletion retry <id>

delete asks you to type the slug before it does anything. --yes skips the prompt for scripts; without it, a non-interactive stdin refuses rather than hanging. By default the CLI performs the teardown itself, so it works on an install whose server is stopped; --queue leaves it for the running server.

It also pauses before starting, and says so. Lyraflow caches each project’s row in memory for a minute, so for that long after you confirm, a running server can still be accepting events for the project out of a cache that has not heard about the deletion — and a teardown that started immediately would drop partitions those events are about to land in. The command waits that window out first. On a default install that is about a minute.

When a deletion fails. A teardown that keeps failing stops being retried after five attempts and reports failed, with the reason in deletion get. The project is then in a half-finished state: it accepts no events, it is gone from every screen but Settings, and whatever survived the teardown is still in ClickHouse — where retention keeps sweeping it, so it is not invisible. deletion retry <id> puts the request back in the queue and the teardown starts again from the top, which is safe to repeat: every step of it is predicated on the project and dropping something already dropped does nothing.

What is missing today

  • No cross-project read. No endpoint aggregates projects, so an “all my sites” total does not exist in the API. Getting one means querying ClickHouse directly, or calling each project in turn and adding up.
  • One project per page. The browser SDK keeps a single configuration on window.lyraflow; calling init() again reconfigures it from scratch rather than adding a second destination. One page cannot report to two projects at once.

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