Hosted dashboard (reference server)

apps/dashboard-reference/ is a self-hostable Next.js app that ingests httpSink POSTs and renders the same pre-built dashboard widgets from gecx-chat/dashboards. Use it when you want persistence across page reloads, a shared ops view, or a starting point for your own analytics surface — without standing up an analytics warehouse.

When to use it

  • You're prototyping or piloting and want a visible health surface without setting up GA4 / Segment / Mixpanel projects.
  • You want a single source of truth for the five canonical metrics (deflection, CSAT, AHT, agent-assist, GMV) before deciding which warehouse to commit to.
  • You want a working reference of how the SDK's httpSink wires to a backend.

It is not an enterprise analytics platform — see "When to outgrow this" below.

Quickstart

# In the SDK repo:
cd apps/dashboard-reference
cp .env.local.example .env.local
# Set DASHBOARD_INGEST_TOKEN to a random string for production.
pnpm install
pnpm dev    # http://localhost:3003

Then point your SDK at it:

import { createChatClient, httpSink } from 'gecx-chat';

createChatClient({
  // ...
  analytics: {
    sink: httpSink({
      url: 'http://localhost:3003/api/events',
      headers: { authorization: 'Bearer your-token' },
    }),
  },
});

The dashboard polls /api/events every 5 seconds — events appear within one poll cycle.

Configuration

Env varDefaultPurpose
DASHBOARD_INGEST_TOKEN(unset)When set, every /api/events* and /api/sessions/* request must include Authorization: Bearer <token>. When unset, the server runs in open mode with a one-time console warning.
DASHBOARD_STORAGEmemorymemory (in-process ring buffer) or sqlite (planned — see below).
DASHBOARD_RING_CAPACITY10000Max events in the in-memory ring buffer. Oldest evicted.
DASHBOARD_WINDOW_DEFAULT7dDefault time window in the UI selector.

The in-memory store is pinned to globalThis so dev-server hot reloads preserve events. It does not survive restarts. For local dev that's fine; for hosted deployments you'll want persistence — either bring your own EventStore implementation (interface in apps/dashboard-reference/src/lib/store.ts) or switch to SQLite via better-sqlite3 (the implementation hook is documented in store.ts but not enabled by default to keep the install footprint small).

Endpoints

MethodPathBody / QueryReturns
POST/api/events{ events: ProductAnalyticsEvent[] }, max 1 MB202 { accepted: N }
GET/api/events?window=7d&limit=10000200 { events, count }
GET/api/sessions/[id]200 { events, count } (sorted asc)
GET/api/health200 { ok: true, eventCount: N } (no auth)

Schema validation is intentionally permissive — events with unknown type values pass through. The dashboard's aggregation primitives ignore event types they don't recognize, so SDK upgrades don't break the server.

Deploy

Vercel. vercel --prod. Set DASHBOARD_INGEST_TOKEN in project env. Cold-start ephemerality means the in-memory ring resets per cold-start instance; for low-volume deployments that's often acceptable. Use a persistent host if you need durability:

Fly.io / Render / Railway. Mount a persistent volume, set DASHBOARD_STORAGE=sqlite and DASHBOARD_SQLITE_PATH=/data/events.db, and ship better-sqlite3 as a runtime dep. The store interface in src/lib/store.ts makes the swap straightforward.

When to outgrow this

The reference server has hard ceilings. Outgrow it when any of these become true:

  • Event volume exceeds ~10M / day (ring buffer churn dominates query latency).
  • You need real auth (OIDC, RBAC, multi-tenant isolation).
  • You need backfill or replay (the ring buffer only holds the latest RING_CAPACITY).
  • You need alerting, anomaly detection, or downstream automation.

When you outgrow it: keep httpSink pointed at your warehouse's ingest endpoint instead. The aggregation primitives (computeDeflectionRate, computeCsat, computeAverageHandleTime, computeAgentAssistRate, computeGmv) are pure functions over ProductAnalyticsEvent[] and run anywhere — your warehouse query layer, a serverless function, or a server component. The widget components accept whatever events you pass them; the data source is yours to choose.

See analytics.md for the broader analytics guide.

Source: docs/guides/hosted-dashboard.md