Implementation notes
How nojs.chat works
The project is a constraint-driven experiment: a modern AI chat experience built from static HTML, ordinary forms, server-side streaming, signed state, and strict output sanitization.
Principles
- No client-side JavaScript in the active product.
- Basic HTML and conservative CSS for ebook-reader-class compatibility.
- Static whenever possible, streamed whenever dynamic.
- Accessible document and form behavior before visual flourish.
Request flow
- Static pageVercel serves
public/index.htmlfrom static infrastructure. - Form postThe browser submits
new_messageto/cwith no fetch, WebSocket, or client runtime. - ValidationThe route checks body size, message size, signed state, prompt budget, and theme.
- Early HTMLThe function starts a
text/htmlstream and sends the document shell before waiting for model text. - Provider streamOpenRouter or DeepSeek returns text chunks through an OpenAI-compatible streaming API.
- Safe renderingThe server converts streamed Markdown into allowlisted HTML and escapes raw model HTML.
- Next turnThe full conversation is trimmed, signed, and embedded as hidden state in the next form.
Decisions and tradeoffs
| Decision | Why | Tradeoff |
|---|---|---|
| No client-side app JavaScript | The core experience works in old browsers, text browsers, locked-down browsers, screen readers, and JavaScript-disabled environments. | No optimistic client UI, no local persistence, and no client Markdown rendering. |
| Static public pages | The homepage and this page are generated at build time into public/, so they do not need a function invocation. |
Changing copy requires a deploy, which is fine for project documentation. |
Node.js route for /c |
The chat route needs request body parsing, provider calls, cancellation, and a streamed HTML response. | It has function latency and provider latency; only the public pages are fully static. |
| Signed hidden state | The app avoids a conversation database while still rejecting tampered history. | State increases form size, so the server trims history and enforces byte limits. |
| Markdown in, safe HTML out | Models are good at Markdown, and the server owns the only HTML that reaches the browser. | Some block formatting appears only once enough Markdown has streamed to render it safely. |
| Strict CSP | The browser should not execute scripts or model-provided markup. | Browser-side performance probes must be reintroduced deliberately, temporarily, and removed again. |
| Direct provider APIs | OpenAI-compatible APIs keep the app small and make provider changes cheap. | The app must handle provider-specific finish reasons, timeouts, and model limits itself. |
Vercel shape
The public pages are build artifacts. The chat endpoint is the only configured function, and it runs on the Node.js runtime because the response is a long-lived stream tied to an upstream model request.
What is intentionally absent
No account system, no browser storage, no database-backed chat history, no analytics script, no client bundle, and no trusted model HTML.
How to prove streaming
Use /c?perf=1 or the local streaming probe. The useful comparison is initial_html_sent versus model_first_chunk. If initial HTML is sent first, the page is not waiting for the full model answer.
npm run measure:streaming -- 'http://localhost:3000/c?perf=1' 'Write a short list about streaming'