Export a subscribe() function from any page or layout to push real-time data to the client over Server-Sent Events. See live demo →
// pages/dashboard.ts export function subscribe({ push }) { const id = setInterval(() => { push({ time: Date.now(), status: 'ok' }); }, 1000); return () => clearInterval(id); } export class PageDashboard extends LitElement { static properties = { time: { type: Number }, status: { type: String }, }; time = 0; status = ''; render() { return html`<p>Server time: ${this.time}</p>`; } }
loader(), the subscribe() function runs only on the server and is stripped from the client bundle. You can safely use database connections, Node.js APIs, and secrets inside it.
1. User navigates to the page. The framework opens an SSE connection to /__nk_subscribe/<path>
2. The server calls subscribe(). Your function starts running and stays alive
3. Call push(data) whenever you want. Data is delivered to the client as an SSE event
4. The client receives the event and spreads each key as an individual property on the component
5. User navigates away. The SSE connection closes, and the cleanup function is called
subscribe() function is not a one-shot handler. It is a persistent process that runs for the entire duration the user is on the page. It can open connections, start loops, listen to events. Anything.
The subscribe function receives a context object with:
| Property | Type | Description |
|---|---|---|
params | object | Dynamic route parameters |
headers | object | Request headers |
locale | string | Current locale (when i18n is configured) |
push | function | Send data to the client. JSON-serialized automatically |
Always return a cleanup function. It is called when the client disconnects (navigates away, closes tab, or loses connection). Use it to close database watchers, clear intervals, and release resources.
export function subscribe({ push }) { const stream = db.orders.watch(); stream.on('change', (change) => { push({ type: 'order-update', data: change }); }); return () => stream.close(); }
Use loader() for initial data and subscribe() for live updates. They work together on the same page:
// pages/orders.ts export async function loader({ params }) { return { orders: await db.orders.findAll() }; } export function subscribe({ push }) { const stream = db.orders.watch(); stream.on('change', (c) => push({ type: 'update', data: c })); return () => stream.close(); } export class PageOrders extends LitElement { static properties = { orders: { type: Array }, type: { type: String }, data: { type: Object }, }; orders: any[] = []; type = ''; data: any = null; render() { return html` <h1>Orders (${this.orders?.length})</h1> ${this.type ? html`<p>Latest: ${this.type}</p>` : html`<p>Waiting for updates...</p>`} `; } }
Layouts can also export subscribe(). Useful for global notifications, presence indicators, or connection status:
// pages/_layout.ts export function subscribe({ push }) { const ws = new WebSocket('wss://notifications.example.com'); ws.on('message', (msg) => { push(JSON.parse(msg)); }); return () => ws.close(); }
Layout subscriptions persist as long as the layout is mounted. If the user navigates between pages that share the same layout, the layout subscription stays active.
Use params to subscribe to route-specific data:
// pages/chat/[room].ts export function subscribe({ params, push }) { const room = chatService.join(params.room); room.on('message', (msg) => push(msg)); return () => room.leave(); }
_subscribe.ts)When the subscribe handler grows complex, move it into a _subscribe.ts file alongside the page. For folder routes (index.ts pages), LumenJS discovers it automatically — no import or wrapper needed in the page file.
pages/
└── orders/
├── index.ts ← page component only
└── _subscribe.ts ← subscribe handler, auto-discovered
// pages/orders/_subscribe.ts export function subscribe({ push }) { const stream = db.orders.watch(); stream.on('change', (c) => push({ type: 'update', data: c })); return () => stream.close(); }
// pages/orders/index.ts — no subscribe() here at all export class PageOrders extends LitElement { static properties = { type: { type: String }, data: { type: Object } }; type = ''; data: any = null; render() { return html`<p>${this.type}</p>`; } }
subscribe(), it takes precedence. Co-located _subscribe.ts only applies to folder routes — flat pages like pages/orders.ts keep the subscribe handler inline.
The same rules as loader() apply:
1. Must be a named export function. Not an arrow function, not a default export
2. Stripped from client bundles automatically
3. Each key from push() is spread as an individual property — same pattern as loader data
4. Transport is SSE. Zero dependencies, native browser EventSource