> Architecture
Single process, single database, zero external dependencies.
Overview
Echelon Analytics is a Deno + Fresh application backed by a single SQLite database. All state — sessions, rate limiter, burst maps, buffered writers — lives in memory. Runs as a single process (no --parallel flag).
Tech Stack
| Layer | Technology |
|---|---|
| Runtime | Deno 2.x |
| Framework | Fresh 2.2.0 (file-system routing, Preact islands) |
| Build | Vite 7 |
| CSS | Tailwind CSS v4 (@tailwindcss/vite plugin) |
| UI | Preact + @preact/signals |
| Database | SQLite via node:sqlite (WAL mode) |
| Container | Docker (multi-stage, non-root, tini) |
Request Flow
How a pageview goes from browser to database:
1. Script Load
Browser fetches /ea.js, which returns a dynamically-generated tracker script with an embedded WASM blob (rotated every 6 hours) and a per-minute challenge string.
2. Client Initialization
- Generates or loads session ID from
sessionStorage - Reads UTM parameters from the URL
- Starts WASM proof-of-work solver asynchronously
- Attaches interaction listeners (pointer, scroll, click, keydown)
- Waits 800ms minimum before firing beacon
3. Pageview Beacon
On user interaction, the tracker loads /b.gif as an image with query params: path, site ID, session ID, PoW token, screen dimensions, referrer, UTM data.
4. Server Processing
- Rate limit check — drop if exceeded
- Known bot UA filter — drop matching requests
- Visitor identity — HMAC hash (cookieless) or
_evcookie (opt-in) - Bot scoring — compute 0–100 heuristic score
- PoW verification — valid (+0), missing (+15), or invalid (+25)
- Data enrichment — geo, device type, OS, referrer classification
- Push to in-memory
BufferedWriter
5. Buffered Write
The BufferedWriter batches records (up to 50k) and flushes to SQLite every 10–15 seconds in a single transaction. On shutdown, remaining records are flushed before exit.
6. Behavioral Events
Scroll depth, clicks, bounces, session events, and custom events are sent via sendBeacon to POST /e. Same scoring and buffering pipeline, separate writer instance.
Database
SQLite Configuration
| PRAGMA | Value | Why |
|---|---|---|
journal_mode | WAL | Concurrent reads during writes |
synchronous | NORMAL | Balance durability and speed |
busy_timeout | 5000 | Retry on lock contention |
foreign_keys | ON | Referential integrity |
Core Tables
| Table | Purpose | Retention |
|---|---|---|
visitor_views | Raw pageviews with bot scores | 90 days |
visitor_views_daily | Rollup aggregates (filtered) | 730 days |
semantic_events | Behavioral events (clicks, scrolls, bounces, custom) | 90 days |
Supporting Tables
| Table | Purpose |
|---|---|
excluded_visitors | Manually excluded visitor IDs |
experiments | A/B experiment metadata |
experiment_variants | Variant definitions and weights |
utm_campaigns | Campaign tracking |
site_settings | Per-site consent CSS |
perf_metrics | CI/CD performance tracking |
maintenance_log | Rollup status tracking |
Adapter Interface
The database layer uses an interface (DbAdapter) with methods for query, queryOne, run, exec, and transaction. Currently backed by node:sqlite. All queries use parameterized ? placeholders.
Data Pipeline
Buffered Writers
Two BufferedWriter<T> instances run in parallel:
| Writer | Target | Flush Interval | Max Buffer |
|---|---|---|---|
| viewWriter | visitor_views | 15 seconds | 50,000 |
| eventWriter | semantic_events | 10 seconds | 50,000 |
Each flush runs in a single transaction. If a flush fails, records are re-added to the buffer.
Daily Rollup
At ~03:00 UTC, the maintenance task:
- Aggregates yesterday's
visitor_viewsintovisitor_views_daily - Filters out
bot_score ≥ 50and excluded visitors - Groups by site, date, device type, country, and returning status
- Uses
INSERT OR REPLACE(idempotent on composite primary key)
Data Purge
After rollup, expired data is purged:
visitor_viewsandsemantic_events: older thanECHELON_RETENTION_DAYS(default 90)visitor_views_daily: older than 730 daysperf_metrics: older than retention period- Followed by
PRAGMA incremental_vacuum(100)
Visitor Identity
Cookieless (default)
HMAC-SHA256 of IP + User-Agent + site ID + date, truncated to 16-char hex. Resets daily — no cross-day tracking. Requires no cookie consent.
Cookie (opt-in)
HttpOnly _ev cookie with a 16-char hex value, 30-day TTL. Enabled with data-cookie on the script tag.
Proof-of-Work System
- WASM module generated from a 64-byte random seed every 6 hours
- Implements a SipHash-inspired algorithm with randomized constants
- Challenge string rotates every minute (HMAC-SHA256 of minute bucket)
- Client solves and caches token in
sessionStorage(90% reuse, 10% re-solve) - 150ms solve timeout — if WASM is too slow, beacon fires without token (penalty applied)
- Server verifies against current + previous WASM slots and last N minute buckets
Authentication
Bearer Token
Set ECHELON_SECRET env var. Verified via constant-time comparison to prevent timing attacks.
Username/Password
PBKDF2-SHA256 with 600k iterations, 256-bit key, 128-bit random salt. Uses Web Crypto API only — no external dependencies.
Sessions
In-memory session store. Random UUID tokens, 24-hour TTL, pruned every 30 minutes. echelon_session HttpOnly cookie.
CSRF Protection
Mutating requests (POST/PATCH/DELETE) with cookie auth must include a matching Origin or Referer header.
Directory Structure
echelon-analytics/
main.ts Entry point, graceful shutdown
deno.json Config, import aliases, tasks
vite.config.ts Vite build config
lib/
tracker.ts Tracker script generation
beacon.ts Pageview beacon handler
events-endpoint.ts Behavioral events handler
bot-score.ts Bot scoring heuristics
challenge.ts PoW challenge management
challenge-wasm.ts WASM bytecode builder
buffered-writer.ts Generic batch writer
auth.ts PBKDF2 password hashing
session.ts In-memory session store
config.ts Env var centralization
maintenance.ts Daily rollup and purge
stats.ts Stats query handlers
utm.ts Campaign cache
rate-limit.ts Per-IP rate limiter
db/
adapter.ts Database interface
sqlite-adapter.ts SQLite implementation
database.ts Init, migrations, singleton
schema.ts DDL and indexes
routes/
ea.js.ts GET /ea.js
b.gif.ts GET /b.gif
e.ts POST /e
_middleware.ts CORS, security headers
api/ REST API routes
admin/ Admin UI routes
islands/ Preact interactive components
components/ Server-only Preact components
assets/ Tailwind CSS, images
static/ Favicon, static files
Design Decisions
Single Process
All state is in-memory. Simple to deploy and reason about. Trade-off: horizontal scaling requires moving to external session/state stores.
WAL Mode
Write-Ahead Logging enables concurrent reads while writes proceed. Combined with synchronous = NORMAL, balances durability and throughput.
Buffered Writes
Trades immediate durability for throughput. Beacon returns instantly (~1ms), records batch-flush every 10–15 seconds. Graceful shutdown flushes remaining buffers.
Runtime WASM Generation
Each deployment generates unique WASM blobs. Bot toolkits can't pre-compute solutions. Low barrier for browsers (150ms timeout with fallback).
Raw Data + Rollups
Raw data kept for inspection and re-analysis (90 days). Rollups provide fast long-range queries (2 years). Bot filtering happens at rollup time — raw data preserves everything for forensics.