Install
$ agentstack add skill-othmanadi-openui-forge-openui-forge-ruby ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo 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 Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
OpenUI Forge — Ruby
Build generative UI apps with a React frontend + Ruby on Rails backend. Streams OpenAI API responses directly via ActionController::Live, forwarding OpenAI's native SSE with Net::HTTP.
Activation Triggers
- "openui ruby", "openui rails", "openui ruby backend"
- "generative ui ruby", "rails streaming ui backend"
Prerequisites
- Node.js >= 22 (24 LTS recommended) + React >= 18.3.1 (19+ recommended) (frontend)
- Ruby >= 3.2 + Rails 8.1.x (backend; run on Puma —
ActionController::Liveneeds a threaded server, not WEBrick) OPENAI_API_KEYenvironment variable set
Quick Start
- Create the React frontend and install OpenUI deps:
npm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
- Generate the system prompt into the Rails app:
npx @openuidev/cli generate ./src/lib/library.ts --out config/system-prompt.txt
- Create the Rails backend (see Full Code below)
- Run:
bin/rails server -p 3001on:3001, frontend on:3000
Full Code
Backend: Gemfile
source "https://rubygems.org"
gem "rails", "~> 8.1"
gem "puma", ">= 6.0"
# Net::HTTP is in the standard library — no extra HTTP-client gem required.
# Optional: load OPENAI_API_KEY etc. from a .env file in development.
gem "dotenv-rails", groups: [:development, :test]
Backend: app/controllers/chat_controller.rb
require "net/http"
require "json"
require "uri"
class ChatController "system", "content" => SYSTEM_PROMPT }]
incoming.each do |m|
next unless m.is_a?(Hash)
messages m["role"].to_s, "content" => m["content"].to_s }
end
# Headers MUST be set before the first write (the response commits on write).
response.headers["Content-Type"] = "text/event-stream"
response.headers["Cache-Control"] = "no-cache"
# Rails inserts Rack::ETag, which buffers the whole body and breaks
# streaming. Setting Last-Modified makes Rack::ETag pass the body through.
response.headers["Last-Modified"] = Time.now.httpdate
# Defeat proxy buffering (nginx) so chunks reach the browser immediately.
response.headers["X-Accel-Buffering"] = "no"
uri = URI.parse("#{OPENAI_BASE_URL}/chat/completions")
payload = JSON.generate(
model: OPENAI_MODEL,
stream: true,
messages: messages,
)
# read_timeout: nil disables the default 60s per-read timeout; a streaming
# completion can pause longer than 60s between chunks and would otherwise
# raise Net::ReadTimeout and cut the response off mid-stream.
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: nil) do |http|
upstream = Net::HTTP::Post.new(uri)
upstream["Content-Type"] = "application/json"
upstream["Authorization"] = "Bearer #{api_key}"
upstream["Accept"] = "text/event-stream"
upstream.body = payload
http.request(upstream) do |res|
unless res.code.to_i == 200
err = +""
res.read_body { |c| err e
Rails.logger.error("[chat] stream error: #{e.class}: #{e.message}")
begin
response.stream.write("data: #{JSON.generate(error: e.message)}\n\n")
response.stream.write("data: [DONE]\n\n")
rescue IOError, Errno::EPIPE
# client already gone
end
ensure
# ALWAYS close, or the socket leaks for the lifetime of the worker.
response.stream.close
end
private
# Lock CORS to the single configured frontend origin (NOT "*"): the request
# is credentialed-capable and a wildcard would let any site spend your key.
def set_cors_headers
response.headers["Access-Control-Allow-Origin"] = ENV.fetch("FRONTEND_ORIGIN", "http://localhost:3000")
response.headers["Vary"] = "Origin"
response.headers["Access-Control-Allow-Methods"] = "POST, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
end
def handle_preflight
head(:no_content) if request.method == "OPTIONS"
end
end
Backend: config/routes.rb
Rails.application.routes.draw do
post "/api/chat", to: "chat#create"
match "/api/chat", to: "chat#create", via: :options # CORS preflight
end
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 Rails backend forwards OpenAI's SSE stream verbatim through ActionController::Live (response.stream.write(chunk) per Net::HTTP read_body chunk), so the client sees tokens as they arrive. Pair it with openAIAdapter() on the frontend. openAIReadableStreamAdapter() is for NDJSON (no data: prefix) and will silently produce no output here. > > Net::HTTP (standard library) is the dependency-free default here. The ruby-openai / openai gems are alternatives if you want a typed client, but forwarding the raw SSE bytes needs no gem.
System Prompt Generation
npx @openuidev/cli generate ./src/lib/library.ts --out config/system-prompt.txt
Validation Checklist
- [ ]
config/system-prompt.txtexists in the Rails app - [ ]
OPENAI_API_KEYis set in environment or.env - [ ] Controller
include ActionController::Liveandskip_forgery_protection - [ ] CORS headers allow the frontend origin (not
*) - [ ] Response streams SSE directly from OpenAI API (verbatim passthrough)
- [ ]
response.stream.closeruns in anensureblock - [ ]
Last-Modified(or removedRack::ETag) so the response is not buffered - [ ] Running on Puma, not WEBrick
- [ ] Frontend
apiUrlpoints tohttp://localhost:3001/api/chat - [ ] Frontend uses
streamProtocol={openAIAdapter()}andopenAIMessageFormat - [ ]
componentLibrary={openuiChatLibrary}prop passed toFullScreen - [ ] CSS import in root layout (
@openuidev/react-ui/components.css)
Error Patterns
| Error | Cause | Fix | |-------|-------|-----| | CORS blocked | Origin mismatch | Set FRONTEND_ORIGIN to match the frontend | | system-prompt.txt missing (Errno::ENOENT at boot) | File not generated | Run the CLI generate command into config/ | | Response arrives all at once, not streamed | Rack::ETag buffering | Keep the Last-Modified header (or remove Rack::ETag) | | Stream hangs / never flushes | Running on WEBrick | Run on Puma (threaded server) | | ActionController::Live::ClientDisconnected / Errno::EPIPE | Client closed the tab mid-stream | Rescue it; the ensure still closes the stream | | Socket stays open after request | response.stream.close skipped | Close in an ensure block (as shown) | | 422 Unprocessable Entity on POST | CSRF check | skip_forgery_protection in the controller | | Empty response | Body not forwarded | Verify the read_body do |chunk| loop writes each chunk |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: OthmanAdi
- Source: OthmanAdi/openui-forge
- License: MIT
- Homepage: https://spruce-prism-8yya.here.now/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.