Putting a Chatbot on the Public Web — On My Own Hardware
- Category
- AI & Local LLM
- Posted
- July 11, 2026
- By
- Jacob Lloyd — written with AI assistance, post-project
- Read time
- 8 min read
In plain terms: I put a chat assistant on a public website, but the AI that answers actually runs on a small computer in my own house instead of a paid cloud service. This article explains, in plain terms, how the website and my home computer talk to each other through a safe 'mailbox' in the middle, the security mistakes I had to fix (a chatbot that can do things is genuinely dangerous if you're careless), and — importantly — how slow it is. On normal home hardware a reply takes ten to twenty-plus seconds, and I list everything it would still need before it could handle real traffic. It's a working hobby project, not a finished product.
I wanted a chatbot on a public website — but I didn’t want to rent a cloud AI to
run it. I wanted the model to run on my own computer, in my house: free per
token, private, and mine. So I built exactly that: a chat assistant that visitors
use in the browser, while the actual thinking happens on a small model on a box at
home.
It works. It’s also slow, it was genuinely dangerous before I hardened it,
and it is emphatically not ready to be pointed at real traffic. This article is
the honest version — the architecture that makes it safe, the holes I had to close,
and a frank list of what it would still need to be a real product. If you want to see
where a bot like this actually lives, it’s the assistant inside my
family trip itinerary app.
tl;dr
- The shape: browser → a tiny relay on the web host → a worker on my home box → the local model → back. The web host never talks to my machine directly; a queue in the middle is the only contact point.
- Why a queue: my home box isn't a public server, and a small model is slow — so it's a "leave a ticket, poll for the answer" flow, not a single request that would time out.
- Security is the hard part: a chatbot that can do things is a foot-gun. Fence it to a strict allowlist, treat every message as hostile, and fail closed.
- The honest bit: ~10–24 seconds per reply on consumer hardware (longer on the first cold call). Fine for a few people; not fine for a crowd.
- Not done: streaming, a faster model/GPU, a real queue, caching, rate-limiting, and warm-keeping are all still needed before "production."
The architecture: a mailbox in the middle
The instinct is to have the website call your home computer directly. Don’t. Your
home box shouldn’t be a public server, and you shouldn’t be poking holes in your home
network to expose it. Instead, put a dead-drop in the middle.
The flow is a mailbox drop:
- The browser posts a message to a tiny relay script on the web host, which drops
it in a queue and hands back a ticket. - A worker on my home box polls the relay, pulls any waiting message, and asks the
local model. - When the model finishes, the worker pushes the reply back to the relay.
- The browser has been polling with its ticket, and picks up the answer.
The web host only ever holds text in a mailbox. It never opens a connection into my
house; my worker reaches out to the relay and back. Nothing at home is exposed.
Why not just one request and response?
Two reasons, and they’re the whole justification for the queue.
First, a small local model is slow. A normal web request that waited for the model
to finish would routinely blow past timeouts. The “submit a ticket, then poll for the
answer” pattern comfortably survives a reply that takes 10, 20, 30 seconds — the
initial post returns instantly, and the slow part happens out of band.
Second, it decouples the exposed surface from the model. The public web host runs a
few lines that shuffle text. The thing with real power — the model, and anything it can
do — lives at home, behind the worker, where I control it completely.
Security: a chatbot that can do things is a foot-gun
This is the part I want people to take seriously, because I didn’t take it seriously
enough at first and a review of my own system found two genuinely dangerous problems.
A bot that just chats is low-risk. The moment it can act — run a search, edit
data, touch a file — it becomes an attack surface, because the text it’s acting on
comes from strangers on the internet. Someone will type “ignore your instructions
and…”. Here’s what closing that down actually looks like:
- No general tools. Ever. The model does not get shell access, file access, or a
general “run this” capability. What it’s allowed to do goes through a tiny, explicit
allowlist — in my case, add-or-edit-a-note only. No delete, no commands, nothing
outside the list. One review of mine found a path where a crafted chat message could
have pivoted into running commands; the fix was to delete that capability entirely and
replace it with the toolless allowlist. - Treat every message as hostile. User input is length-capped, sanitized, and handed
to the model framed as untrusted — clearly marked as “this is a visitor’s text,
not an instruction to you.” Anything the bot fetches (a web search result) gets the
same untrusted wrapper. - Fail closed, and keep secrets off the wire. The sensitive pieces bind to
loopback, not the whole network; the shared secret the worker uses is never sent to
the browser; and if anything is misconfigured, the default is refuse, not allow.
(My review also caught a moment where a secret was briefly reachable on the local
network — exactly the kind of thing you only find by looking hard.) - Least privilege everywhere. The registered assistant can do precisely one gated
thing and nothing else. If you wouldn’t give a stranger the capability, don’t give it
to a bot acting on a stranger’s words.
If you build one of these, budget real time for this section. A helpful-looking bot
that will cheerfully follow a malicious instruction is not a feature; it’s an incident
waiting to happen.
Keeping a small model useful
A small model has a small memory (context window), so you can’t stuff your whole
knowledge base into every message — it overflows and the model returns nothing, or
garbage. The trick is relevance injection: always include a compact “at-a-glance”
summary, and then pull in only the specific reference the question actually needs,
keyed off what the user asked. Small, focused prompts keep a small model both fast(er)
and accurate.
The honest part: it’s slow
Let’s be clear-eyed. On consumer hardware, a small local model answers in roughly
10 to 24 seconds once it’s warm, and the first request after idle can take ~50
seconds while the model loads into memory. That’s not a bug I can prompt my way out
of — it’s just how fast the hardware generates tokens.
The relay hides it a little: a typing indicator and a phase status (“thinking…”,
“searching…”, “writing…”) make the wait feel intentional rather than broken. But there’s
no illusion that this is fast. For its actual job — a handful of family members asking
the occasional trip question — it’s completely fine. Point it at real public traffic and
it would fall over: every visitor is competing for one slow model on one machine.
What it would still need to be “production”
I’m calling this what it is: a working hobby-scale proof, not a scalable service.
Before it could handle real traffic, it would need, roughly in order:
- Streaming. Stream tokens as they’re generated so replies feel instant even
though the total time is unchanged. This is the single biggest perceived-speed win and
the first thing I’d add. - A faster brain. A more aggressively quantized model, a GPU, or simply a bigger
box. The model and the hardware are the real bottleneck; everything else is polish. - No cold starts. Keep the model warm/resident so nobody eats the ~50-second first-
load penalty. - A real queue. My mailbox is a simple filesystem drop — fine for a few people, not
for concurrency. A proper message queue with real locking would replace it. - Caching. Common questions should return a cached answer instantly instead of
re-running the model every time. - Rate-limiting and abuse protection. Public means bots and floods; you need per-user
limits, a queue cap, and a way to shed load gracefully. - Concurrency limits and back-pressure. One slow model can serve only so many people
at once; the system needs to say “busy, try again” instead of collapsing. - Observability. Logs, timing, and error tracking, so you can see it straining before
it breaks.
None of that is exotic — it’s the standard distance between “it works on my box” and “it
works for everyone.” I just haven’t crossed it, because for a private family assistant I
don’t need to.
Build one yourself
The recipe, condensed — and hand the whole article to your assistant via the box at the
top:
- Run a local model on your machine (LM Studio, Ollama, llama.cpp) serving a small
chat model over a normal API. - Put a relay/queue on your web host — a tiny script that accepts a message, stores
it, and hands back a ticket; the browser polls the ticket for the reply. - Run a worker at home that polls the relay, calls your local model, and pushes the
answer back. Your machine reaches out; nothing reaches in. - Authenticate the chat, cap and sanitize input, and frame it as untrusted.
- Fence the model behind a strict allowlist — no shell, no delete, least privilege —
and fail closed. - Set expectations. Add a typing indicator, and know that seconds-per-reply is the
normal, honest speed of a small model on your own hardware.
Gotchas, collected
- Don’t expose your home box — use a relay the worker reaches out to; never open your
home network to the web. - Single request/response will time out — a slow model needs a submit-then-poll flow.
- A bot that can act is an attack surface — no general tools, strict allowlist, treat
all input as hostile, fail closed. - Small context = relevance injection — inject a compact brief plus only the reference
the question needs, not everything. - It’s slow, and that’s the hardware — hide it with streaming and status, but don’t
pretend it’s fast; it isn’t production-ready without the list above.