Share a streaming AI chatbot with a reviewer
Checked against the qURL™ CLI 3.5.0 release and documentation on October 18, 2026. Link and session behaviour is tested separately.
Content updated October 21, 2026. The verification date above records the original review; later behaviour checks are dated in the guide.
How do I let someone try my local AI chatbot, with replies that stream?
Cloudflare Quick Tunnels do not support Server-Sent Events, so they are a poor fit for a local chatbot that streams replies over SSE. qURL can share the chat interface instead: on October 18, 2026, with qurl 3.5.0, a reply streamed word by word in a browser opening the qURL link and Stop ended generation on the server. Keep the model’s API key in your backend and share the interface, not the model API.
What you need
- A chat app with a backend that streams replies, or Node.js to run the sample below (we used Node.js 20.18)
- The qURL CLI 3.4.1 or later
- No account, API key, or sign-in for qURL
Steps
Working in a coding agent? Paste the prompt instead
In Claude Code, Cursor, Codex or another agent that can run commands, paste this in the project whose chat app is running. The agent installs the CLI, publishes the app privately and asks who should get in. The steps below do the same by hand.
Read https://layerv.ai/skills/qurl/SKILL.md and use qURL to share the app I'm running locally.Install the qURL CLI
On macOS, install it with Homebrew. On Linux and Windows, download the package for your platform from the latest release; the CLI page lists each one. Nothing asks you to sign up.
brew install layervai/tap/qurl qurl --versionStart the chat app, or the sample
The sample is one file with no dependencies. Its backend streams fixture replies one word at a time as Server-Sent Events, the way model APIs stream tokens, and its page has a Stop button. It listens on 127.0.0.1:47614, not on your network. Skip this step if your own chat app is running. The sample accepts local and qURL Host headers. For another sharing tool, forward Host: localhost or add its exact assigned hostname to the sample’s check; a 403 “Host not allowed” is not a streaming failure.
curl -fsSLO https://layerv.ai/data/qurl-streaming-chat/server.cjs node server.cjsKeep the model key on the server
Share the chat interface, not the model. The browser should talk only to your backend, and your backend calls the model provider with the key from its own environment. In the sample, that call replaces fixtureReply() in server.cjs. A local model server such as Ollama stays bound to localhost; only the chat app is published.
Publish the chat app
The first publish creates this device’s identity, starts a background service that keeps serving after you close the terminal (while you stay logged in), and prints the app’s CRID, its stable ID. The Access row says who can open the app: a plain publish makes it private from qurl 3.4.1 on. qurl share turns the CRID into an access link, and the link is what you send.
qurl publish http://127.0.0.1:47614Create a link with a longer session
A demo usually needs more than the default 1-hour session. The link itself still opens once and expires within 24 hours.
qurl share <CRID> --session-duration 2hSend the link to one person
Send it in a direct message, not a public channel: whoever opens it first uses it. Avoid email and apps that preview links, because a scanner that runs the page could use its one opening. Don’t open it yourself to check it, for the same reason. Run qurl share again whenever you need a fresh link.
Check that replies stream for the reviewer
Ask the reviewer to send a message. The reply should appear word by word. If it arrives all at once at the end, something between the model and the browser is buffering the response; see Troubleshooting. If your interface has a Stop button, check that pressing it also stops generation on your server, so an abandoned reply does not keep spending tokens.
End the demo
Stop serving the app with qurl stop; qurl start resumes it. qurl delete removes the resource for good after asking you to confirm, and its CRID can never be shared again.
qurl stop <CRID> qurl delete <CRID>
What the recipient sees
The reviewer opens the link and lands on the chat at an address like https://r_….qurl.site/, with no warning page, sign-up or install. Replies appear as they are generated. In our October 18 test with qurl 3.5.0, words the sample sent 150 ms apart arrived 152 ms apart at the median, and pressing Stop closed the request on the local server one word later. Your app’s own login still applies.
Which tool fits a streaming chatbot demo?
| Option | Choose it when | Trade-off |
|---|---|---|
| qURL | qURL fits when a reviewer should use your chat interface in their own browser: in our October 18 test, a streamed reply arrived word by word through a qURL link. | With qURL your computer, the model and the app must stay running, and each qurl share link opens once. |
| Cloudflare Quick Tunnels | A Cloudflare Quick Tunnel fits a chat app that streams over WebSockets rather than Server-Sent Events, when you want no account. | Cloudflare Quick Tunnels do not support Server-Sent Events, so a reply streamed over SSE stalls; the URL is also public unless you set --allowed-mail. |
| ngrok | ngrok fits when you already use it and want the same URL for every demo; ngrok’s docs say WebSocket connections work with no configuration. | ngrok’s docs do not mention Server-Sent Events, so test streaming before the demo; the URL is public unless a policy restricts it. |
| zgrok | zgrok fits when the reviewer calls your local model from their own OpenAI-compatible client: zgrok ai shares the model’s API behind a bearer token and waits for slow first tokens. | zgrok shares the raw model API, not your chat interface, so the reviewer needs a client and the token, and can send the model any request. |
Troubleshooting
| If you see | Do this |
|---|---|
| The reply appears all at once at the end | Something is buffering the stream. Send each event as soon as it is ready with Content-Type: text/event-stream and Cache-Control: no-cache, and turn off response compression for that route. On a Cloudflare Quick Tunnel, Server-Sent Events are not supported at all. |
| The page loads but no reply ever starts | Check your backend’s log for the provider call. A key read from a .env file the server never loaded is the usual cause; the browser cannot fix it, because it never sees the key. |
| The chat UI is a Vite or Next.js dev server and shows “Blocked request” or never updates | Allow qURL’s hostnames first: .qurl.site in Vite’s server.allowedHosts, or *.qurl.site in Next.js allowedDevOrigins. The client review guide has the config. |
| qurl publish says the local app is not accepting TCP connections | Start the app first and check that curl works on the same address. qurl publish connects to localhost as 127.0.0.1, so if your app listens only on IPv6 (on macOS or Linux, lsof -iTCP -sTCP:LISTEN -nP shows [::1]), publish 'http://[::1]:PORT' instead. Keep the quotes: zsh treats the brackets as a pattern. |
| The recipient sees “Access link invalid” | The link was already used or has expired. Run qurl share again and send the new link. |
| The recipient sees “Resource not found” | The resource was deleted after the link was made. Publish the app again, then run qurl share and send the new link. |
| publish rejects http://127.0.0.1 with “only HTTPS URLs are allowed”, or asks you to log in | You have a CLI older than 3.0. On macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. After updating, qurl --version should print 3.4.1 or later. |
| publish prints no Access: line, or the Access: line says public and you did not ask for public | With no Access: line, the CLI is older than 3.4.1: on macOS run brew update, then brew upgrade qurl; elsewhere install the latest release again. A resource that an older CLI published is public unless you published it with --private, and anyone who has the CRID of a public resource can open it. To make a public resource private, run qurl delete <CRID> and publish again with qurl 3.4.1 or later. The new resource is private and has a new CRID. |
| publish says this Linux session “can’t reach a systemd user manager” | You are in a container or sandbox without a user service manager. Run qurl publish with --foreground; sharing lasts as long as that process runs. |
| publish says “Quota Exceeded” with “protected resource limit reached”, or “Your qURL account has reached a plan limit” | You can see this without an account: a device with no account can keep up to 3 active resources, with links that last up to 24 hours. Run qurl list --status active, then qurl delete <CRID> for any you no longer need (it asks you to confirm; --yes skips that). qurl stop keeps the resource, so it does not free a slot. |
Frequently Asked Questions
Do Cloudflare Quick Tunnels support Server-Sent Events?
No. Cloudflare’s Quick Tunnels page says they do not support Server-Sent Events. The limit is documented for Quick Tunnels; Cloudflare’s Tunnel FAQ says Cloudflare Tunnel supports WebSockets, so a chat app that streams over a WebSocket is a different case.
Should I share the chat interface or the model’s API?
Share the interface when a person is trying your app: they use it as you built it, and your backend decides what reaches the model. Share the API only when the reviewer will call the model from their own client, and then protect it with a token, as zgrok ai does. A raw model endpoint lets whoever has access send it any request.
Does Stop cancel generation through qURL?
In our October 18 test, yes. Pressing Stop aborted the browser’s request, the connection closed through the qURL link, and the sample server stopped sending one word later. Your backend has to listen for that close and cancel its provider call, as the sample does.
Does my computer have to stay on?
Yes. The app is served from your machine, so the machine and the app must be running and connected. The background service reconnects after sleep, but the app is unavailable while the machine is asleep.
Who else can reach my app while the link is open?
Opening the link admits the network address it was opened from for the session, which lasts 1 hour by default. Other devices that share that public IP address, such as people on the same office or café network or VPN, can reach the app until the session ends. By default a link admits whoever opens it; access rules set through the API, such as IP allow and deny lists, can restrict that. Keep your app’s own sign-in for anything sensitive.
How do I stop sharing?
Run qurl stop <CRID> to pause serving the app, and qurl start <CRID> to resume it; a stopped app still counts toward your resource limit. qurl delete <CRID> removes the resource for good after asking you to confirm; its CRID can never be shared again.