2026-05-14 22:44:14 -07:00
# user-data
Parent feature: cross-domain, cross-corp visitor flow on the lilith-platform.live mesh.
2026-05-14 23:06:58 -07:00
Absorbs what used to be `@features/analytics/` and adds two missing layers — a
server-side cookie-free visitor identity and a first-class corp/domain taxonomy
— so a visitor's journey across `adulttherapytour.com → maisonsansonnet.com →
transquinnftw.com` becomes a single SQL join. Deployed surface: [` quinn.data`](../../../deployments/@domains/quinn .data/)
(data.transquinnftw.com).
2026-05-14 22:44:14 -07:00
## Shape
| Path | Package | Role |
|---|---|---|
| `shared/` | `@lilith/analytics-client` | Browser client core — batch queue, attribution, device collector. No React dep. |
| `frontend-client/` | `@lilith/analytics-client-react` | React provider + hooks (`useAnalytics` , `useFunnelEvents` , `usePageViewTracking` ). |
2026-06-10 21:33:02 -07:00
| `website-backend-users/` | `@lilith/analytics-website-backend` | Stateless BFF proxy. Forwards `/track/*` to the collector (or quinn-api relay) and `/api/*` (after composition) to quinn-api analytics query surface. Both dev and prod use quinn-api + the prod lilith_analytics DB (no dev DB). Does NOT do identity — that lives in the collector. |
2026-05-14 23:06:58 -07:00
| `website-frontend-users/` | `@lilith/analytics-website-users` | Dashboard SPA at `data.transquinnftw.com/` . Cross-Corp Flow tab is TODO. |
| `beacon/beacon.js` | (single static file, no package) | ~4 KB cookie-free IIFE for static-HTML sites. Served by quinn.data nginx at `https://data.transquinnftw.com/beacon.js` . Staged into the SPA dist by `quinn.data/deploy.sh` step 1a. |
## Identity & dimensioning — lives in the collector, NOT here
The identity hash and corp/domain resolution run inside the @analytics collector
2026-06-10 21:33:02 -07:00
(or via quinn-api public analytics relay in consolidated paths)
2026-05-14 23:06:58 -07:00
at `~/Code/@applications/@analytics/services/collector/src/tracking/` :
- `identity.service.ts` — `visitorIdDaily(ip, ua, lang) = sha256(daily_salt ‖ ip ‖ ua ‖ lang)` . Salt rotates at 00:00 UTC, stored in `visitor_salts` , purgeable after 7 days.
- `domain-resolver.service.ts` — Origin → `domains` row → `(corp_id, domain_id)` . Cached in-process; reloaded on cache miss.
- Both are wired into `TrackingService.trackView()` and stamp every `raw_events` row.
Schema lives in `@applications/@analytics/services/api/migrations/1747200000000-AddVisitorIdentityAndCorpDomain.ts` . Seed mirror: `@applications/@analytics/infrastructure/seed-cross-domain.sql` .
This split is intentional: identity logic has exactly one consumer (the collector),
so publishing it as a `@lilith/*` package would be premature abstraction.
2026-05-14 22:44:14 -07:00
## Visitor identity model
- **No cookies.** No localStorage. No fingerprint canvas.
- `visitor_id_daily = sha256(salt_today ‖ X-Real-IP ‖ User-Agent ‖ Accept-Language)` .
2026-05-14 23:06:58 -07:00
- Same visitor → same id across all our domains within a UTC day.
- Salt rotates at 00:00 UTC; older salts are purged → historical re-identification
is mathematically impossible.
2026-05-14 22:44:14 -07:00
- Tab-scoped `sessionId` (client-side, in-memory) is preserved for per-tab funnels.
## Corp / domain taxonomy
2026-05-14 23:06:58 -07:00
Every event row carries `corp_id` and `domain_id` . Seed:
2026-05-14 22:44:14 -07:00
2026-05-14 23:06:58 -07:00
| corp slug | legal_name |
2026-05-14 22:44:14 -07:00
|---|---|
| `lilith-apps-ehf` | Lilith Apps ehf |
| `att` | Adult Therapy Tour |
| `sansonnet` | Maison Sansonnet |
| `transquinnftw` | transquinnftw |
2026-05-14 23:06:58 -07:00
Seeded domains include `adulttherapytour.com` , `adulttherapy.tours` , `apa.singles` ,
`fuckatapa.com` , `fuckmeatamericanpsychiatricassociation.com` , `maisonsansonnet.com` ,
`transquinnftw.com` , `tqftw.com` , `atlilith.com` , `trustedmeet.com` . See the
migration file for the full list.
## Instrumented sites
| Site | Instrumentation | Domain row |
|---|---|---|
| Landing | React provider (`AnalyticsProvider` ) | `atlilith.com` , `trustedmeet.com` |
| Provider website (transquinnftw.com) | React provider | `transquinnftw.com` |
| ATT canonical (`adulttherapytour.com` ) | `<script>` beacon in 4 HTMLs under `adult-therapy-tours/web/` | `adulttherapytour.com` |
| ATT SEO bait | `<script>` beacon in `apa-singles` , `fuckatapa` , `fuckmeat` | per hostname |
2026-05-15 23:14:08 -07:00
| Sansonnet (preview, pre-prod) | `<script>` beacon in 4 HTMLs under `.project/previews/cocotte-umbrella/sites/sansonnet/` | `maisonsansonnet.com` |
2026-05-14 23:06:58 -07:00
2026-05-15 21:19:17 -07:00
Not yet instrumented: `futa-singles` SEO surface (no live domain yet), `cocotte`
2026-05-14 23:06:58 -07:00
brand under the sansonnet umbrella.
## Known gaps
2026-06-10 21:33:02 -07:00
- The rich dashboard query surface (sessions/metrics, engagement/pages, funnels, etc.)
is now implemented inside quinn-api (using direct SQL against the prod lilith_analytics DB).
The old separate Nest @analytics query API (:4003) is no longer used by the website
analytics BFF/dashboard.
2026-05-14 23:06:58 -07:00
- **CORS for `/beacon.js` consumers** — verify on canary that the collector at
`:4001` accepts POSTs from the new origins (`adulttherapytour.com` , `apa.singles` ,
`fuckatapa.com` , `fuckmeatamericanpsychiatricassociation.com` , `maisonsansonnet.com` ).
The existing analytics-client at `atlilith.com` and `transquinnftw.com` already
works, suggesting the collector emits `Access-Control-Allow-Origin: *` — but
confirm before declaring done.
- **Cross-Corp Flow dashboard tab** is not yet built (deferred to a follow-on objective).
- **Pre-existing typecheck error** in `tracking.controller.ts` line 59 (`doNotTrack`
type mismatch in `ClientDeviceDto` ) — unrelated to this work but blocks `tsc --noEmit`
on the collector. Skipped in this objective.
2026-05-14 22:44:14 -07:00
2026-05-14 23:06:58 -07:00
## Privacy posture
2026-05-14 22:44:14 -07:00
2026-05-14 23:06:58 -07:00
- No cookies, no localStorage, no fingerprinting.
- IP is processed (for the daily hash) but never stored — only the 32-byte sha256
digest persists alongside events.
- `device-enrichment.service` separately stores an `ipHash` for geo/bot detection;
that is unchanged and unrelated to `visitor_id_daily` .