Stream server
evlog ships a tiny HTTP server that exposes the in-process stream over Server-Sent Events. It runs in the same Node process as your app, on its own ephemeral port — your API surface is untouched, and any consumer (browser tab, CLI, Tauri/Electron devtool) can subscribe.
pnpm dev, on a Node / Bun / Deno container, on a long-lived VM, on Fly / Railway / Coolify-style instances.What boots up
When the stream is enabled, evlog calls startStreamServer() and:
- Opens a
node:httpserver bound to127.0.0.1on an OS-assigned ephemeral port. - Subscribes the SSE connections to the default in-process stream — every wide event flows through.
- Writes the URL to
<cwd>/.evlog/stream.urlso external tools can discover the port. - Prints a banner at startup:
[evlog] Stream → http://127.0.0.1:51203
- Cleans up the URL file and closes the server on
SIGINT,SIGTERM, and process exit.
Per framework
Nuxt — zero config in dev
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
// stream auto-on in dev — banner appears at `pnpm dev`
})
To override:
evlog: {
stream: true, // force-enable in dev AND prod
// stream: false, // explicit off
// stream: { port: 4317, token: '...' }, // custom config
}
The Nuxt module also registers a tiny /api/_evlog/stream-info route that reads .evlog/stream.url and returns the URL — useful when the consumer is a page on the same Nuxt app and needs to discover the mini-server's ephemeral port.
Next.js (instrumentation.ts)
import { defineStreamedInstrumentation } from 'evlog/next/stream'
export const { register, onRequestError } = defineStreamedInstrumentation({
service: 'my-app',
// stream auto-on when NODE_ENV === 'development'
})
import { defineNodeInstrumentation } from 'evlog/next/instrumentation'
export const { register, onRequestError } = defineNodeInstrumentation(() =>
import('./lib/evlog')
)
In dev, the same banner prints when Next boots the Node runtime. The stream server's drain is composed with any user-provided drain so events keep flowing to your other adapters too.
Standalone Node / Bun / Deno script
import { startStreamServer } from 'evlog/stream'
import { initLogger } from 'evlog'
const server = await startStreamServer()
initLogger({ drain: server.drain })
// ... your script runs, devtools can subscribe ...
Hono / Express / Fastify / Elysia / NestJS / SvelteKit
There's no auto-wiring for these frameworks yet. The 3-line manual setup is the same as the standalone Node case — call startStreamServer() once during boot and pass server.drain wherever you compose your evlog drain.
API
import { startStreamServer, type StreamServer, type StreamServerOptions } from 'evlog/stream'
const server: StreamServer = await startStreamServer({
port: 0, // 0 = OS picks ephemeral port (default)
host: '127.0.0.1', // default — local-only, never exposed to LAN
token: 'optional-bearer', // default: none (origin check used instead)
heartbeatMs: 15_000, // default
buffer: 500, // default ring buffer size
banner: true, // default — prints `[evlog] Stream → ...`
urlFileDir: '.evlog', // default — false to disable .evlog/stream.url
})
server.url // → 'http://127.0.0.1:51203'
server.port // → 51203
server.drain // DrainFn — pass to nitroApp.hooks.hook('evlog:drain', drain) or initLogger({ drain })
server.stream // StreamDrain (the underlying in-process pub/sub)
await server.close() // stop, remove .evlog/stream.url, unsubscribe clients
startStreamServer() is idempotent — calling it again returns the same instance until close() is called.
Endpoints
| Path | Purpose |
|---|---|
GET / | The SSE stream itself. Accepts ?since=<iso> to replay buffered events. |
GET /info | JSON { evlogVersion, bufferSize, heartbeatMs } — server discovery. |
OPTIONS * | CORS preflight (the server allows * because it binds to localhost). |
Wire format
Every SSE data: line is a versioned envelope:
data: {"evlog":"1","type":"hello","data":{"evlogVersion":"2.16.0","bufferSize":500,"heartbeatMs":15000}}
data: {"evlog":"1","type":"replay","data":{...wide event...}}
data: {"evlog":"1","type":"event","data":{...wide event...}}
event: ping
data: {"evlog":"1","type":"ping","data":{"t":1730000000000}}
| Type | When |
|---|---|
hello | First frame — server version + stream config |
replay | Each buffered event flushed when the client passed ?since= |
event | Each new event drained after the connection opened |
ping | Heartbeat every heartbeatMs (default 15s), sent with event: ping |
The evlog: "1" discriminant is the protocol version — incompatible changes will bump it.
Auth model
| Mode | Behavior |
|---|---|
token set | Authorization: Bearer <token> is required. 401 otherwise. |
token unset, request has no Origin (curl, Node fetch) | Allowed. |
token unset, request Origin is local (localhost, 127.0.0.1, ::1) | Allowed. |
token unset, request Origin is non-local | 403. |
The server binds to 127.0.0.1 by default — non-local hosts can't reach it at all unless you override host. The token is only useful when you intentionally expose the server to a wider network.
Discovery
External tools (a Tauri devtool, a CLI watcher) can find the running server in two ways:
.evlog/stream.url— read directly from the project directory. Cleaned up at process exit.GET /api/_evlog/stream-info(Nuxt only) — returns{ url }, reads from the file.
# CLI consumer
URL=$(cat .evlog/stream.url) && curl -N "$URL"
Going further
- Recipes — copy-paste examples for browser, curl + jq, Node fetch, replay-then-live, aggregation.
- Stream API — the in-process primitive the server is built on.