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

  1. No client-side JavaScript in the active product.
  2. Basic HTML and conservative CSS for ebook-reader-class compatibility.
  3. Static whenever possible, streamed whenever dynamic.
  4. Accessible document and form behavior before visual flourish.

Request flow

  1. Static pageVercel serves public/index.html from static infrastructure.
  2. Form postThe browser submits new_message to /c with no fetch, WebSocket, or client runtime.
  3. ValidationThe route checks body size, message size, signed state, prompt budget, and theme.
  4. Early HTMLThe function starts a text/html stream and sends the document shell before waiting for model text.
  5. Provider streamOpenRouter or DeepSeek returns text chunks through an OpenAI-compatible streaming API.
  6. Safe renderingThe server converts streamed Markdown into allowlisted HTML and escapes raw model HTML.
  7. 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'