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
- Start with SQLite or MySQL and
TIDEMARK_INGEST=sync .
- If writes slow your pages, set
TIDEMARK_INGEST=queue and add the cron job.
- Move the analytics tables to their own server with
TIDEMARK_ANALYTICS_DB .
- Use retention to cap table growth.