Architecture

browser ──► POST /api/event ──► Collector ──► EventSink ──► analytics DB (visits, events, event_props) │ ├─ DatabaseSink (default: write in-request, stitches visits) │ └─ QueueSink (buffer →…

 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.
Was this article helpful?
Last updated 10 Nov 2026
Powered by Tidemark Billing