HyperGen

Getting Started

Get HyperGen running in 5 minutes. Copy two files, set up an SSE endpoint, mount an iframe, and stream your first AI-generated UI.

HyperGen is an open protocol that lets AI agents generate interactive user interfaces by streaming HTML fragments with HTMX attributes over Server-Sent Events (SSE) into sandboxed iframes. There are no SDKs to install, no JSON schemas to learn, and no framework lock-in. Any system that can output an HTML string can use HyperGen.

Prerequisites

  • Runtime: Node.js 20+, Deno 2+, or Bun 1+
  • HTTP framework: Anything that supports the standard Response API (Hono, Express, Next.js Route Handlers, Deno.serve, etc.)
  • Frontend: Any framework (React, Vue, Svelte) or vanilla HTML/JS

Step 1: Copy the Drop-in Files

HyperGen follows the code ownership model: you copy source files into your project and own them. There is no npm install.

Copy these files from packages/dropin/src/:

FileWherePurpose
hypergen-server.tsYour serverSSE streaming, bootstrap HTML generation
hypergen-client.tsYour frontendIframe mounting, theme injection, lifecycle
hypergen-theme.tsEither (optional)TypeScript types for CSS variables

Both files have zero external dependencies -- they use only standard Web APIs.

Step 2: Set Up the Server

You need two HTTP endpoints:

  1. Bootstrap endpoint -- serves the HTML document that goes inside the iframe
  2. SSE endpoint -- streams HTML fragments to the iframe
// server.ts
import { bootstrapHtml, createSSEStream, HyperGenResponse } from "./hypergen-server";

// Serve the iframe bootstrap document
app.get("/bootstrap", () => {
  return new Response(
    bootstrapHtml({ sseEndpoint: "/api/stream" }),
    { headers: { "Content-Type": "text/html" } },
  );
});

// Stream HTML fragments via SSE
app.get("/api/stream", () => {
  const stream = createSSEStream(async function* () {
    yield "<h1>Hello from HyperGen!</h1>";
    yield "<p>This HTML was streamed via SSE.</p>";
  });

  return HyperGenResponse(stream);
});

bootstrapHtml() generates a complete HTML document with HTMX loaded from CDN, the SSE extension wired up, a postMessage bridge for host communication, and a ResizeObserver for auto-sizing.

createSSEStream() takes an async generator of HTML strings and returns a ReadableStream with proper SSE formatting and keep-alive pings.

HyperGenResponse() wraps the stream in a Response with correct SSE headers.

Step 3: Mount the Iframe in Your Frontend

// client.ts
import { mountHyperGen } from "./hypergen-client";

const controller = mountHyperGen(
  document.getElementById("agent-ui")!,
  { bootstrapUrl: "/bootstrap" },
);

// Optional: inject your app's theme
controller.setTheme({
  "--hg-accent": "#7c3aed",
  "--hg-surface": "#ffffff",
});

// Optional: react to content size changes
controller.onResize((height) => {
  console.log("Content height:", height);
});

mountHyperGen() creates a sandboxed <iframe> with sandbox="allow-scripts allow-same-origin allow-forms", loads the bootstrap document, and returns a HyperGenController for managing the iframe lifecycle.

Step 4: Generate HTML from Your Agent

The server-side async generator is where your agent produces UI. Each yield sends one SSE event containing an HTML fragment that HTMX renders into the iframe.

app.get("/api/stream", () => {
  const stream = createSSEStream(async function* () {
    // Call your AI agent
    const response = await agent.chat("Show me a dashboard");

    // Stream the HTML it generates
    yield response.html;

    // Or build HTML from structured data
    yield `
      <div style="padding: 16px; background: var(--hg-surface-elevated); border-radius: var(--hg-radius);">
        <h2 style="color: var(--hg-text);">Dashboard</h2>
        <button
          hx-post="/api/action"
          hx-vals='{"action": "refresh"}'
          hx-target="closest div"
          hx-swap="outerHTML"
          style="background: var(--hg-accent); color: var(--hg-accent-fg); border: none; padding: 8px 16px; border-radius: var(--hg-radius-sm); cursor: pointer;">
          Refresh
        </button>
      </div>
    `;
  });

  return HyperGenResponse(stream);
});

// Handle user interactions sent by HTMX
app.post("/api/action", async (c) => {
  const body = await c.req.parseBody();
  // Return new HTML that HTMX swaps in
  return c.html("<div>Updated at " + new Date().toLocaleTimeString() + "</div>");
});

The key insight: the agent's output IS the UI. No JSON translation, no component catalog lookup -- just HTML with HTMX attributes for interactivity and --hg-* CSS variables for theming.

Complete Minimal Example

Here is a complete working example using Hono (swap in any framework):

import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { bootstrapHtml, createSSEStream, HyperGenResponse } from "./hypergen-server";

const app = new Hono();

app.get("/", (c) => c.html(`<!DOCTYPE html>
<html><head><title>HyperGen Demo</title></head>
<body>
  <h1>My App</h1>
  <div id="agent-ui"></div>
  <script type="module">
    import { mountHyperGen } from "/hypergen-client.js";
    mountHyperGen(document.getElementById("agent-ui"), {
      bootstrapUrl: "/bootstrap",
    });
  </script>
</body></html>`));

app.get("/bootstrap", (c) => c.html(
  bootstrapHtml({ sseEndpoint: "/api/stream", swapStrategy: "beforeend" })
));

app.get("/api/stream", () => HyperGenResponse(
  createSSEStream(async function* () {
    yield '<p style="color: var(--hg-text);">Loading dashboard...</p>';
    await new Promise((r) => setTimeout(r, 1000));
    yield `<div style="background: var(--hg-surface-elevated); padding: 16px; border-radius: var(--hg-radius); border: 1px solid var(--hg-border);">
      <h2>Welcome</h2>
      <p>This UI was streamed by an AI agent.</p>
      <button hx-post="/api/action" hx-swap="outerHTML"
              style="background: var(--hg-accent); color: var(--hg-accent-fg); border: none; padding: 8px 16px; border-radius: var(--hg-radius-sm); cursor: pointer;">
        Click me
      </button>
    </div>`;
  })
));

app.post("/api/action", (c) => c.html(
  '<div style="color: var(--hg-accent); font-weight: bold;">Button clicked!</div>'
));

serve({ fetch: app.fetch, port: 3000 });

What's Next

On this page