Every analytics dashboard lives in a browser tab. My terminal is always open. So I built @zenovay/cli, a terminal interface for my analytics SaaS.
The killer command is zenovay events tail: a live stream of pageviews and events, filterable, pipeable, scriptable. Think stripe listen but for your web traffic.
This post covers how the live tail actually works.
The product surface
$ zenovay events tail --site mysite.com --path /pricing
[14:32:18] mysite.com pageview /pricing US Chrome
[14:32:21] mysite.com pageview /pricing DE Firefox
[14:32:24] mysite.com click /pricing#cta-pro US Chrome
[14:32:28] mysite.com pageview /pricing GB Safari
You can pipe it:
$ zenovay events tail --site mysite.com | jq .country | sort | uniq -c
The architecture
Three components:
- CLI binary (TypeScript, distributed via npm)
- WebSocket gateway (Cloudflare Worker with Durable Objects)
- Event broadcast (Workers pushing to subscribed Durable Objects)
The Durable Object as fanout point
Each tracked site gets a Durable Object that holds the list of active WebSocket subscribers.
export class SiteStream {
constructor(private state: DurableObjectState) {
this.sockets = new Set<WebSocket>()
}
async fetch(request: Request) {
if (request.headers.get('Upgrade') === 'websocket') {
const pair = new WebSocketPair()
this.handleSocket(pair[1])
return new Response(null, {
status: 101,
webSocket: pair[0]
})
}
if (request.method === 'POST') {
const event = await request.json()
this.broadcast(event)
return new Response('ok')
}
}
broadcast(event: unknown) {
const data = JSON.stringify(event)
for (const ws of this.sockets) {
try { ws.send(data) }
catch { this.sockets.delete(ws) }
}
}
}
The CLI side
Built with Commander for argument parsing and just plain WebSocket for the stream. Catppuccin Mocha palette via chalk hex codes.
Install
npm install -g @zenovay/cli
zenovay auth login
zenovay events tail --site yoursite.com
Free tier of Zenovay includes the CLI. Site is zenovay.com.
Anyone else building TUIs for SaaS products? Curious what patterns you have found.
Valerio
Top comments (0)