> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.hyjal.cloud/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.hyjal.cloud/_mcp/server.

# Stream server-sent events

> Build an SSE endpoint on the JavaScript lane and consume it from a browser page with EventSource, streaming events over a single HTTP response.

This tutorial builds a server-sent events (SSE) endpoint on the JavaScript lane
and consumes it from a browser page with `EventSource`. You will stream a
sequence of events over one long-lived HTTP response and see them arrive
incrementally in the client. It takes about fifteen minutes.

[Afterburner](/get-started/what-is-hyjal) streams response bodies natively, so
SSE works without special configuration. Long-lived duplex sockets are not the
model; SSE plus `POST` covers the push cases. See
[Streaming and realtime](/app-types/streaming-and-realtime) for the full
picture.

## Prerequisites

* The [`hyjal` CLI](/reference/cli) installed and logged in.
* Node.js installed locally, for the JS lane build.
* A Project. This tutorial uses `acme` with an `api` Resource.

## 1. Write the SSE handler

The JavaScript lane contract is a fetch handler:
`export default { fetch(request) }`. Return a `Response` whose body is a
`ReadableStream` and whose `Content-Type` is `text/event-stream`. Each SSE
message is a `data:` line terminated by a blank line.

```js
// src/index.js
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname !== "/events") {
      return new Response("not found", { status: 404 });
    }

    const encoder = new TextEncoder();
    const stream = new ReadableStream({
      start(controller) {
        let n = 0;
        const send = () => {
          const payload = JSON.stringify({ n, at: Date.now() });
          controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
          n += 1;
          if (n >= 5) {
            controller.close();
            return;
          }
          setTimeout(send, 1000);
        };
        send();
      },
    });

    return new Response(stream, {
      headers: {
        "content-type": "text/event-stream",
        "cache-control": "no-cache",
      },
    });
  },
};
```

This handler emits five events, one per second, then closes the stream. Keep
each streamed run within the Resource's [run limit](/platform/limits)
(`runLimitSecs`, default 30 seconds).

## 2. Configure the Resource

Set the `api` Resource to the JS lane and mark it public so the browser can
reach the endpoint.

```toml
[project]
name = "acme"

[resource.api]
lane = "js"
public = true
```

## 3. Test locally with hyjal dev

Run the local dev server. It serves your component under the same wasi:http
contract as production, with hot rebuild and request logging in the terminal.

```bash
$ hyjal dev --port 3000
Building resource api (lane: js)...
  componentize-js ✓
Serving acme/api on http://localhost:3000
Watching for changes...
```

In another terminal, stream the endpoint with `curl`. The `-N` flag disables
buffering so events print as they arrive.

```bash
$ curl -N http://localhost:3000/events
data: {"n":0,"at":1752412920000}

data: {"n":1,"at":1752412921000}

data: {"n":2,"at":1752412922000}

data: {"n":3,"at":1752412923000}

data: {"n":4,"at":1752412924000}
```

The one-second gap between lines confirms the response is streaming, not
buffered and flushed at the end. See [`hyjal dev`](/develop/hyjal-dev) for more
on the local server.

## 4. Consume from a browser page

Add a static `web` Resource that serves an HTML page. The page opens an
`EventSource` against the `api` endpoint and appends each event to the DOM.

```html
<!-- public/index.html -->
<!doctype html>
<meta charset="utf-8" />
<title>SSE demo</title>
<ul id="log"></ul>
<script>
  const log = document.getElementById("log");
  const es = new EventSource("https://api--acme.hyjal.cloud/events");
  es.onmessage = (event) => {
    const item = document.createElement("li");
    item.textContent = event.data;
    log.appendChild(item);
  };
  es.onerror = () => es.close();
</script>
```

Add the front-end Resource to the manifest and mount the API behind it so the
page and endpoint share an origin. Same-origin dispatch removes the need for
any CORS configuration.

```toml
[resource.web]
lane = "static"
output = "public"
mounts = { "/events" = "api" }
```

With the mount in place, point `EventSource` at the same-origin path instead
of the absolute URL:

```js
const es = new EventSource("/events");
```

## 5. Deploy and verify

Deploy both Resources.

```bash
$ hyjal deploy
Building resource api (lane: js)... ✓
Building resource web (lane: static)... ✓
Uploading artifacts... ✓
Applying policy... ✓
Flipping routes... ✓

Deployed acme/web  https://web--acme.hyjal.cloud
Deployed acme/api  https://api--acme.hyjal.cloud
```

Open `https://web--acme.hyjal.cloud`. The list fills in one row per second as
events arrive over the streamed response.

## What you built

* A JavaScript-lane SSE endpoint that streams events over one HTTP response.
* A browser page that consumes the stream with `EventSource`.
* A same-origin `mounts` wiring that removes CORS configuration from the
  client.

## Next steps

* [Streaming and realtime](/app-types/streaming-and-realtime): the streaming
  model and when to use SSE plus `POST`.
* [Full-stack apps](/app-types/full-stack-apps): `mounts` and in-process
  dispatch between Resources.
* [JavaScript and TypeScript](/languages/javascript-typescript): the JS lane
  and supported frameworks.