Architecture

 browser ──► POST /api/event ──► Collector ──► EventSink ──► analytics DB (visits, events, event_props)
                                    │           ├─ DatabaseSink  (default: write in-request, stitches visits)
                                    │           └─ QueueSink     (buffer → worker → DatabaseSink)
 dashboard (Preact, no build) ──► JSON API ──► Stats engine (Period + Filters) ──► analytics DB
                                                └─ metadata DB (users, sites, goals, shared links, shields)

Design choices

  • Visits, not sessions. One row per visit, stitched on write with a 30-minute inactivity timeout.
  • Two logical databases. Hot, append-only tables use TIDEMARK_ANALYTICS_DB . Everything else uses the default connection. Tables are keyed by site_id  and indexed on time.
  • Stateless app tier. Sessions and cache can move to Redis or the database, and any number of PHP workers can serve requests.
  • Swappable seams. EventSink  (writes) and Stats\Stats  (reads) are the only places that know about the storage engine. A columnar implementation can replace them without touching controllers or the UI.
  • No build step. The dashboard is Preact loaded as plain modules.

Growing a self-hosted installation

  1. Start with SQLite or MySQL and TIDEMARK_INGEST=sync .
  2. If writes slow your pages, set TIDEMARK_INGEST=queue  and add the cron job.
  3. Move the analytics tables to their own server with TIDEMARK_ANALYTICS_DB .
  4. Use retention to cap table growth.