# Why we built Linda: forms aren't the bottleneck, agents are
We started with a server-side conversational form filler. We ripped it all out and rebuilt as a browser-first agent SDK. The pivot, the bet, and where AI actually lives on the web.
**Published:** 2026-05-28  
**Tags:** product, story, agents, browser  
**Cluster:** narrative  
---
A year ago, this codebase was something called `lichat` — a server-side conversational form filler. Express, multer, sharp, a Postgres backend, a chat UI that talked to OpenAI over a thin Node proxy. It worked. It was also a 70 MB Docker image solving a problem the browser could already solve.

We deleted it.

What's left is Linda — a browser-first agent SDK that does what `lichat` did, minus everything we no longer needed. No backend. No multer. No sharp. ~35 KB of TypeScript that turns a form into a chat that fills the form.

This is the story of why that pivot was right, what we learned along the way, and where we think AI agents on the web are actually going.

## The original premise

`lichat` was built on a simple insight: long forms have terrible conversion. A conversational interface (one question at a time, with feedback and validation) converts way better than a 12-field wall of inputs.

So we built one. The architecture made sense for 2023: chat in the browser, conversation state on the server, LLM call from the server, file uploads server-side. Standard SaaS shape.

We shipped it. It worked. Real teams replaced real forms with `lichat` and got better completion rates.

And every single integration project ran into the same wall.

## The wall

Integration pattern A: "Can you talk to our CRM?"  
Sure — we'll write a connector for your CRM and run it server-side.

Integration pattern B: "Can the agent handle this file the user uploads?"  
Sure — we'll add multer, sharp, a PDF parser, and a worker queue.

Integration pattern C: "Can the agent see this part of our app and answer questions about it?"  
Hmm. We'd need a way for your app to push state into our server. So we'd build a websocket. And an SDK to instrument your state. And...

Integration pattern D: "We can't put our user data in your cloud — can it run in our infra?"  
Yes, here's the Helm chart. The Helm chart got more complex than the agent.

Every conversation ended with us *building infrastructure around the agent* instead of building the agent. Forms were the easy part. The integration was the hard part. And every integration looked like the same shape: pull data from where the user already has it, into our server, so our agent can use it.

That's a sign you're solving the wrong problem.

## The architectural reframing

Where does the user have all the data the agent needs?

The browser tab they're in.

The CRM record is rendered on the page they're looking at. The PDF they care about is on their desktop. The form they're trying to fill is in the DOM. The state from your React app is already in memory. The user is already authenticated. The fonts are loaded. The privacy posture is already negotiated.

What if the agent ran *there*?

## The new bet

Three realizations made the rewrite obvious.

**One.** Modern LLMs are trained on filesystem-shaped workflows. Claude Code, agentic repo edits, MCP. They're remarkably good at `ls`, `cat`, `grep`, and editing files. The shape of "give the model a workspace and let it navigate" is what they're best at.

**Two.** Modern browsers can do almost everything. pdf.js parses PDFs. WebGPU runs small models. WebAuthn handles auth. WebUSB / WebSerial / WebNFC handle hardware. The browser is a perfectly capable runtime — it's just rarely treated as one.

**Three.** SSE works in the browser. The model's responses can stream token-by-token via `fetch` + `ReadableStream`. No backend needed for the streaming UX.

Stack those three and the design falls out:

- Give the model the live page as a virtual filesystem.
- Write a real tool loop (tree, read, write, ls, grep, complete) that operates on it.
- Stream the model's responses to the user.
- Make file parsing, browser APIs, and external data sources all mount under the same VFS.
- Ship it as a script tag.

That's Linda.

## What we kept

Surprisingly little code survived. We kept:

- The conversational pattern (one question at a time, warm).
- The mental model of "data points to collect" → "rules to validate" → "target to fill."
- The Shadow-DOM chat UI, ported and slimmed down.

We added:

- The virtual filesystem (didn't exist before).
- The 19-event hook registry (was 5 events in `lichat`).
- The skill system (was hardcoded prompts in `lichat`).
- Multi-agent handoffs (didn't exist).
- MCP, both client and server (didn't exist).
- Browser-side parsers (was server-side multer+sharp).
- Multi-provider transports (was OpenAI only).

The result is smaller. ~35 KB of TypeScript. No Docker image, no Postgres, no Helm chart. The whole runtime is a static file.

## What this changes for users

The blast radius shifts. In the old model, every integration touched our infra. In the new model:

- **Your data stays yours.** Parsed PDFs never leave the browser. The LLM provider sees what your prompts send; you control that.
- **Your auth stays yours.** The user is logged into your app; Linda runs inside it.
- **Your CSS stays yours.** Shadow DOM doesn't fight your styles.
- **Your stack stays yours.** SSE through a 1 KB Node proxy if you want to hide your API key; nothing if you don't.

There's no Neul Labs cloud between you and your LLM provider. That's the operating definition of "open SDK" vs. "open core SaaS."

## What we got wrong about chatbots

`lichat`'s pitch was "replace your form with a chat." Everybody nodded. Then they asked for everything else. The chat was easy. The agent was what they wanted.

A chatbot widget is a stateless UI over a chat-completion API. It answers questions. It can't *do* much. The moment you want it to operate on your app — fill the form, read the document, hand off to a specialist, integrate with your CRM — you need an agent. With a workspace. With tools. With a loop. With skills.

We built the chatbot first. Then realized everyone wanted the agent. So we rebuilt for the agent and let the chat be a presentation layer.

## What Linda is, in one sentence

> Linda is what you'd build if you wanted Claude Code, but the workspace was the page the user is on.

That's the whole shape. The rest — the providers, the parsers, the multi-agent — is implementation detail.

## What's next

We're cutting v1.0 in the next month. That freezes the public API and removes the "0.x" caveat. After that:

- More skills. Calendar-booking, address-verify, KYC, payment-intake all ship today. Next up: contract intake, accessibility audit, support-ticket triage.
- More mounts. Today: clipboard, geo, file system, web speech, wake lock, notifications, camera, mic, barcode, NFC, WebAuthn, Bluetooth, USB, serial. We'll add MIDI, ambient light, screen capture refinements.
- More transports. We have five providers and OpenAI-compatible baseUrl. We'll add the AWS Bedrock invoke path and Azure-specific token routing.
- A dashboard. Optional. For teams that want a place to inspect conversation logs across users. Strictly observability, not a SaaS gate.

If you've been here from the `lichat` days — thanks for sticking around. If you're new — welcome to Linda. The 10-second demo is in [/install](/install). The bigger picture is in [/how-it-works](/how-it-works).

Talk to us at [linda@neullabs.com](mailto:linda@neullabs.com) or open a thread on [GitHub Discussions](https://github.com/neul-labs/linda/discussions).