Two engines. Six shapes. One ingest endpoint.
Traced is intentionally simple at the data layer: every event your SDK fires flows through one HTTPS endpoint and lands in one of two tables. The complexity is on the viewer side, where the same primitives compose into every report you see.
Data flow
Unity SDK
│ batched POST (gzipped JSON, ~5s flush)
▼
Edge Function /v1/ingest
│ API key auth → project lookup → tier check
▼
Postgres
├─ events (one row per Track* call)
└─ position_samples (one row per TrackPosition call, Pro+ only)
│
│ Supabase Realtime
▼
Dashboard (Next.js + Three.js)
├─ Spatial Density engine → DBSCAN cluster heatmap
└─ Path Flow engine → Catmull-Rom curvesThe two engines
Spatial Density
Input: a stream of (x, y, z) positions from events. The viewer bins them into uniform spatial cells (default 1.6m cube), then renders each occupied cell as a translucent sphere whose color and size encode density. The uniqueness-weighted mode (default) prefers cells touched by many distinct sessions over cells where one session emitted many events — defends against farming or AFK bots distorting the heatmap.
Path Flow
Input: position_samples grouped by (session_id, player_id). Each group is sorted by timestamp, decimated to ≤500 samples, and rendered as a Catmull-Rom curve in a teal→cyan hue range. Different players take adjacent hue slots so they read as distinct.
Reports = templates over engines
Death Heatmap, Kill Heatmap, Combat Hotspots, Player Paths — these aren't separate code. They're JSON configs that parameterize one of the two engines with a filter, an accent color, and an icon. Adding a new template = adding a config entry. Adding a new engine is the only thing that requires renderer work.
Backend stack
| Layer | Tech |
|---|---|
| Postgres + RLS | Supabase (managed) |
| HTTP ingest | Deno Edge Function |
| Realtime push | Supabase Realtime (Phoenix Channels) |
| Object storage | Supabase Storage (for uploaded maps) |
| Scheduled jobs | pg_cron (retention purges) |
| Auth | Supabase Auth (email + password; OAuth roadmap) |
Frontend stack
| Layer | Tech |
|---|---|
| Framework | Next.js 14 (app router, client-side only) |
| 3D viewer | Three.js (custom orbit controls, no addon deps) |
| Styling | Tailwind + custom component classes |
| Type | Geist Sans / Geist Mono |
| Auth | @supabase/supabase-js (RLS does the gating) |
Security posture
- RLS: every public table has Row-Level Security enabled. The dashboard uses the anon key + the user's JWT; RLS policies tie reads to
org_membersmembership. The ingest function uses service-role (RLS bypass) since it authenticates via X-API-Key. - Beta gate: new signups land with
approved=false. The dashboard shows a waitlist screen until an admin flips the flag. - Tier enforcement: position streams are rejected for free-tier projects with HTTP 403 at the ingest function. Other tier caps will land before public launch.
- API keys: stored plaintext (they're scoped tokens, not user secrets — revocability bounds the blast radius). Test-tier keys committed in public docs are deliberate.
Performance
- SDK: zero allocations in steady-state TrackEvent calls; ring buffer reuses slots. ~0.1ms per frame upper bound.
- Ingest: ~10ms p50 latency Unity → Postgres on us-west-1; gzip cuts payload ~5×.
- Realtime: ~100ms websocket fanout from insert to dashboard render.
- Viewer: heatmap rebuild is O(N) where N is the bin count; 5000 events → ~120 bins → 16ms.
Roadmap notes
Architecture v1.0 documented an older 2D-first design. The current product is 3D-first per a 2026-05-13 design decision. Position streams shipped in MVP rather than Phase 2. The per-event style map is in production, with icon billboards landing 2026-05-15. See the changelog for the full release timeline.