Installation

Get Echelon Analytics running in under a minute.


Prerequisites

Quick Start

🖥️ "No Deno yet? curl -fsSL https://deno.land/install.sh | sh and you're good to go on Linux and macOS." -🦭

git clone https://github.com/janit/ea.git
cd ea/echelon-analytics
deno task dev

The dev server starts on http://localhost:1947 with Vite HMR.

Add the Tracker

Drop this script tag into any page you want to track:

<script async src="https://your-host/ea.js" data-site="my-site"></script>

That's it. Pageviews, bounces, and sessions are tracked automatically.

Content Security Policy

If your site sends a Content-Security-Policy header, it must let the tracker reach your Echelon host: the host in script-src (to load /ea.js), in img-src (the /b.gif pixel) and in connect-src (for /c and /e), plus 'wasm-unsafe-eval' in script-src so the proof-of-work module can run:

script-src 'self' https://your-host 'wasm-unsafe-eval';
connect-src 'self' https://your-host;
img-src 'self' https://your-host

Without 'wasm-unsafe-eval' the tracker still works but sends no token, so its requests carry the +30 missing-token penalty instead of being discarded at ingest. A view with no token and no interaction is later raised to the bot threshold by the engagement gate (see Bot Defense).

Development Commands

All commands run from echelon-analytics/:

# Development server (Vite HMR)
deno task dev

# Production build
deno task build

# Start production server (must build first)
deno task start

# Check formatting, lint, and type-check
deno task check

# Individual checks
deno fmt --check .
deno lint .
deno check main.ts

# Update Fresh framework
deno task update

# Start MCP server (read-only analytics for AI agents)
deno task mcp

Docker

Multi-stage build with non-root user and health check:

# Build
docker build -f confs/Dockerfile -t echelon-analytics .

# Run
docker run -p 1947:1947 -v echelon-data:/app/data echelon-analytics

The container exposes port 1947. Mount a volume at /app/data/ for the SQLite database (required for WAL mode to work correctly).

Docker Details

Docker with Build Args

docker build -f confs/Dockerfile \
  --build-arg VERSION=1.0.0 \
  --build-arg GIT_HASH=$(git rev-parse --short HEAD) \
  -t echelon-analytics .

Reverse Proxy

The application already sets Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options on HTML responses. Do not duplicate those in your proxy — it causes double headers. The proxy should add headers the app does not set: Strict-Transport-Security, Referrer-Policy, Permissions-Policy, and strip the Server header.

Set ECHELON_TRUST_PROXY=true when behind a reverse proxy to read the real client IP from X-Forwarded-For / X-Real-IP.

Caddy

echelon.example.com {
    reverse_proxy localhost:1947

    encode gzip zstd

    header {
        # Only headers the app does NOT set.
        # Do NOT add CSP, X-Frame-Options, or X-Content-Type-Options here.
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        Referrer-Policy strict-origin-when-cross-origin
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
        -Server
    }

    @tracking {
        path /b.gif /e /api/ingest
    }
    request_body @tracking {
        max_size 64KB
    }
}

A full example is in confs/Caddyfile.example.

Nginx

server {
    listen 443 ssl;
    server_name echelon.example.com;

    location / {
        proxy_pass http://127.0.0.1:1947;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Only headers the app does NOT set.
    # Do NOT add CSP, X-Frame-Options, or X-Content-Type-Options here.
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    server_tokens off;

    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml;
}

Cloudflare

When deployed behind Cloudflare, set:

ECHELON_BEHIND_CLOUDFLARE=true
ECHELON_TRUST_PROXY=true

This enables reading Cloudflare headers (cf-ipcountry, cf-connecting-ip) for geo data and bot scoring.

Authentication Setup

API Token

ECHELON_SECRET=your-secret-token-here

Use with Authorization: Bearer your-secret-token-here header.

Username/Password

# Generate a password hash
cd echelon-analytics
deno eval "import{hashPassword}from'./lib/auth.ts';console.log(await hashPassword('yourpassword'))"

# Set environment variables
ECHELON_USERNAME=admin
ECHELON_PASSWORD_HASH=pbkdf2$600000$<salt>$<hash>

Both auth modes can be used simultaneously.

🔐 "Did you know both API token and username/password auth can be used simultaneously? Use the bearer token for CI/CD pipelines and password login for your team's dashboard." -🦭

Upgrading

Upgrading to a version with validated visitors and the engagement gate (from one without them) changes three things on the first start:

Upgrading to a version with the device, cross-site and geography signals:

Single-Process Constraint

Echelon Analytics keeps all state in memory (sessions, rate limiter, burst maps, buffered writers). You must run with a single Deno worker — do not use the --parallel flag with deno serve.


Features API Reference Bot Defense Configuration Architecture Data Ownership & Open Access Portable Data MCP Server Telemetry Why ea.js?