AgentStack
SKILL verified MIT Self-run

Openui Forge Elixir

skill-othmanadi-openui-forge-openui-forge-elixir · by OthmanAdi

OpenUI generative UI with Elixir Phoenix backend. Chunked SSE streaming via Plug.Conn and Req.

No reviews yet
0 installs
14 views
0.0% view→install

Install

$ agentstack add skill-othmanadi-openui-forge-openui-forge-elixir

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Openui Forge Elixir? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

OpenUI Forge — Elixir

Build generative UI apps with a React frontend + Elixir Phoenix backend. Streams OpenAI API responses directly to the browser as SSE using Plug.Conn.send_chunked/2 and the Req HTTP client's streaming :into collector.

Activation Triggers

  • "openui elixir", "openui phoenix", "openui elixir backend"
  • "generative ui elixir", "elixir streaming ui backend", "phoenix sse openui"

Prerequisites

  • Node.js >= 22 (24 LTS recommended) + React >= 18.3.1 (19+ recommended) (frontend)
  • Elixir >= 1.15 with Erlang/OTP 25+ (Phoenix 1.8 baseline)
  • Phoenix ~> 1.8 (current stable; 1.8.8 as of June 2026)
  • OPENAI_API_KEY environment variable set

Quick Start

  1. Create the React frontend and install OpenUI deps:
npm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
  1. Generate the system prompt into the Phoenix app's priv/:
npx @openuidev/cli generate ./src/lib/library.ts --out backend/priv/system-prompt.txt
  1. Create the Phoenix backend (see Full Code below)
  2. Run: mix deps.get && mix phx.server on :4000, frontend on :3000

Full Code

Backend: backend/mix.exs (deps)

defp deps do
  [
    {:phoenix, "~> 1.8.0"},
    {:bandit, "~> 1.0"},      # HTTP server (Phoenix 1.8 default)
    {:jason, "~> 1.4"},       # JSON encode/decode
    {:req, "~> 0.6"},         # HTTP client with streaming :into (v0.6.2+)
    {:cors_plug, "~> 3.0"}    # explicit-origin CORS
  ]
end

Backend: load the system prompt once at startup

Read priv/system-prompt.txt a single time and cache it in :persistent_term. Call OpenuiBackend.load_system_prompt!/0 from your Application.start/2 callback before the endpoint child starts.

# lib/openui_backend.ex
defmodule OpenuiBackend do
  @key {__MODULE__, :system_prompt}

  def load_system_prompt! do
    path = Application.app_dir(:openui_backend, "priv/system-prompt.txt")

    case File.read(path) do
      {:ok, contents} ->
        :persistent_term.put(@key, contents)
        :ok

      {:error, reason} ->
        raise "failed to read #{path}: #{:file.format_error(reason)}"
    end
  end

  def system_prompt, do: :persistent_term.get(@key)
end
# lib/openui_backend/application.ex — inside start/2, before the children list
def start(_type, _args) do
  OpenuiBackend.load_system_prompt!()

  children = [
    OpenuiBackendWeb.Endpoint
    # ...
  ]

  Supervisor.start_link(children, strategy: :one_for_one, name: OpenuiBackend.Supervisor)
end

Backend: lib/openui_backend_web/controllers/chat_controller.ex

defmodule OpenuiBackendWeb.ChatController do
  use OpenuiBackendWeb, :controller

  require Logger

  @openai_chat_path "/chat/completions"

  # POST /api/chat
  def create(conn, %{"messages" => messages}) when is_list(messages) do
    case System.get_env("OPENAI_API_KEY") do
      key when is_binary(key) and key != "" -> stream_chat(conn, messages, key)
      _ -> send_error(conn, 500, "OPENAI_API_KEY not set")
    end
  end

  def create(conn, _params), do: send_error(conn, 400, "messages must be a non-empty array")

  defp stream_chat(conn, messages, api_key) do
    base_url = System.get_env("OPENAI_BASE_URL") || "https://api.openai.com/v1"
    model = System.get_env("OPENAI_MODEL") || "gpt-5.5"

    # Prepend the server-side system prompt; never trust the client to send it.
    system_message = %{"role" => "system", "content" => OpenuiBackend.system_prompt()}
    payload = %{"model" => model, "stream" => true, "messages" => [system_message | messages]}

    # Open the SSE response before the upstream call so the first byte flushes
    # to the browser as soon as OpenAI starts emitting tokens.
    conn =
      conn
      |> put_resp_content_type("text/event-stream")
      |> put_resp_header("cache-control", "no-cache")
      |> put_resp_header("x-accel-buffering", "no")
      |> send_chunked(200)

    result =
      Req.post(
        url: base_url <> @openai_chat_path,
        json: payload,
        auth: {:bearer, api_key},
        receive_timeout: :infinity,
        # `:into` turns the response body into a stream: Req hands each upstream
        # SSE chunk to this 2-arity collector as {:data, data}. We forward it
        # verbatim with chunk/2 and halt the moment the client disconnects,
        # mirroring the Enum.reduce_while halt-on-{:error,_} pattern. Plug.Conn
        # is immutable, so we thread the latest conn through resp.private.
        into: fn {:data, data}, {req, resp} ->
          out_conn = resp.private[:openui_conn] || conn

          case Plug.Conn.chunk(out_conn, data) do
            {:ok, out_conn} ->
              {:cont, {req, Req.Response.put_private(resp, :openui_conn, out_conn)}}

            {:error, reason} ->
              Logger.debug("client disconnected mid-stream: #{inspect(reason)}")
              {:halt, {req, resp}}
          end
        end
      )

    case result do
      {:ok, %Req.Response{status: status} = resp} when status in 200..299 ->
        # OpenAI already sends a terminating `data: [DONE]` line (forwarded verbatim).
        resp.private[:openui_conn] || conn

      {:ok, %Req.Response{status: status}} ->
        Logger.error("OpenAI returned HTTP #{status}")
        push_error_frame(conn, "OpenAI returned HTTP #{status}")

      {:error, exception} ->
        Logger.error("OpenAI request failed: #{Exception.message(exception)}")
        push_error_frame(conn, "OpenAI request unreachable")
    end
  end

  defp push_error_frame(conn, message) do
    payload = Jason.encode!(%{error: message})
    _ = Plug.Conn.chunk(conn, "data: #{payload}\n\ndata: [DONE]\n\n")
    conn
  end

  defp send_error(conn, status, message) do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{error: message}))
  end
end

Backend: lib/openui_backend_web/router.ex (scope)

defmodule OpenuiBackendWeb.Router do
  use OpenuiBackendWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/api", OpenuiBackendWeb do
    pipe_through :api

    post "/chat", ChatController, :create
  end
end

CORS lives in the endpoint, before the router, locked to the frontend origin (never a wildcard for the credentialed case):

# lib/openui_backend_web/endpoint.ex — before `plug OpenuiBackendWeb.Router`
plug CORSPlug,
  origin: [System.get_env("FRONTEND_ORIGIN") || "http://localhost:3000"],
  methods: ["POST", "OPTIONS"]

Frontend: app/chat/page.tsx

"use client";
import { FullScreen } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import {
  openAIAdapter,
  openAIMessageFormat,
} from "@openuidev/react-headless";

export default function ChatPage() {
  return (
    
  );
}

> The Phoenix backend proxies OpenAI's native SSE stream verbatim: Req's :into collector receives each upstream {:data, data} chunk and re-emits it with Plug.Conn.chunk/2 (incremental flush, no buffering), so the browser sees data: {chunk}\n\n lines and the terminating data: [DONE]. Pair it with openAIAdapter() on the frontend. openAIReadableStreamAdapter() is for NDJSON (no data: prefix) and will silently produce no output here. > > req_llm (built on Req + Finch) is a higher-level alternative that handles provider SSE adaptation if you later want typed multi-provider support. This skill keeps plain Req as the dependency-light default.

System Prompt Generation

npx @openuidev/cli generate ./src/lib/library.ts --out backend/priv/system-prompt.txt

Validation Checklist

  • [ ] priv/system-prompt.txt exists in the Phoenix backend
  • [ ] OpenuiBackend.load_system_prompt!/0 is called in Application.start/2 before the endpoint
  • [ ] OPENAI_API_KEY is set in environment (honor OPENAI_BASE_URL for OpenAI-compatible providers)
  • [ ] CORS plug allows the frontend origin explicitly (not *)
  • [ ] Response streams SSE directly from OpenAI API (Req :into passthrough, chunk/2 per chunk)
  • [ ] Frontend apiUrl points to http://localhost:4000/api/chat
  • [ ] Frontend uses streamProtocol={openAIAdapter()} and openAIMessageFormat
  • [ ] componentLibrary={openuiChatLibrary} prop passed to FullScreen
  • [ ] CSS import in root layout (@openuidev/react-ui/components.css)

Error Patterns

| Error | Cause | Fix | |-------|-------|-----| | CORS blocked | Origin mismatch | Set FRONTEND_ORIGIN and confirm CORSPlug runs before the router | | :persistent_term key not found | Prompt not loaded at boot | Call OpenuiBackend.load_system_prompt!/0 in Application.start/2 | | (File.Error) could not read priv/system-prompt.txt | File missing | Run the CLI generate command into priv/ | | 500 OPENAI_API_KEY not set | Env var missing | Export OPENAI_API_KEY before mix phx.server | | Empty response | Wrong frontend adapter | Backend emits SSE; use openAIAdapter(), not openAIReadableStreamAdapter() | | Stream stalls / cut at ~60s | Cowboy idle timeout | On Bandit (default) no cap; on Cowboy set protocol_options: [idle_timeout: :infinity] | | function send_chunked/2 undefined | Plug.Conn not imported | Use use OpenuiBackendWeb, :controller (imports Plug.Conn) or call Plug.Conn.send_chunked/2 |

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.