teaflask

How to add an AI copilot to your SaaS product: the iceberg under the chatbot

Ragav Sachdeva26 Sep 2026

You build an in-product copilot in one weekend. Nothing exotic: an AI SDK, a model key, a chat panel docked in the corner of the app. You spend the week polishing it, and on Friday you demo it to the rest of the company, and it is lovely. The text streams in like it's being typed by someone confident. It knows what the product is. When it's asked something it can't do, it apologizes in exactly the right tone. Someone says “ship it.” Someone else asks how you built it so fast, and you say, truthfully, “mostly the SDK.”

Then Monday comes, and the questions start arriving. Not all at once, which would at least be efficient. One at a time, each from a different person, each completely reasonable. Whose permissions does it call our API with? What happens when someone closes the tab halfway through a task? Who approves the refund? Why is it describing a setting we renamed last sprint? Those four are the real answer to a plainer question, and it's the one this post exists to answer: how does adding an AI copilot to a SaaS product actually work?

It works in two parts. The visible part is a chat panel wired to a language model, and a framework gets you there in a weekend. The part that decides whether you can ship it is everything the panel needs before it's allowed near real users and real data: identity, permissions, durable turns, knowledge that tracks your code, model fallbacks, and a UI that survives customers. We'll go through all of it, say which teams should build it themselves (some of you should), and leave room for the voice in your head that says “nahhh.”

The demo that works on Friday

Start with why the demo works, because we'll need the reason in a minute. A demo is a system with exactly one user, who is you, on a good connection, in one tab, asking questions whose answers you already know, against a staging account where nothing you do can hurt anyone. Every hard property of production has been quietly set to its easiest value. The demo is honest enough; nobody has asked it anything hard yet.

The first question comes from the security lead, and it's short. When the copilot calls your API to look up an invoice, whose credentials does it use? The honest answer from the weekend build is “the key in the environment variable.” One key, with its own broad access, shared by every person who will ever open the panel. The security lead goes quiet in the particular way that means a meeting has just been scheduled.

The second comes from support. A customer asked the copilot to reconcile a batch of records, which took a while, so they closed the laptop and went to lunch. When they came back the conversation was blank and nobody could say whether the records had been touched. (Some had. Not all. That's the worst number.)

The third comes from finance, and it ends the optimism. The copilot can issue refunds now, because that was the impressive part of the demo. Who approves them? Nobody, it turns out. The model decides, and the model is polite but not accountable.

The fourth arrives as a customer's support ticket, pasted into the team channel. The copilot told them to open “Billing contacts.” That screen became “Invoice recipients” in last sprint's release. Strictly speaking the copilot wasn't hallucinating, since it was faithfully describing a product that no longer exists.

None of these is a bug in the AI SDK; each is a question the SDK was never meant to answer, and each is a layer of work that was invisible on Friday because the demo never touched it.

Fig. 1 — The Friday demo, and the questions waiting behind it

The iceberg under the chatbot

You know the picture: a bit of ice above the water, most of it below, and a ship that only measured the part it could see. Software teams have lived a version of this forever (the demo, the production system, the gap between them), but copilots get an unusually dramatic one, because the visible part is so persuasive. Streaming text looks like intelligence. It looks finished.

So call it the iceberg under the chatbot. Above the waterline sits everything a demo can prove: a panel, a model call, a system prompt, tokens appearing on screen. Below it sits everything production asks for afterward. The waterline is the line between what a demo can show you and what real users, with real accounts and real deadlines, will eventually demand of it.

Two things about this particular iceberg are worth knowing before we dive. Every layer under the water is load-bearing, so removing one doesn't give you a smaller assistant, it gives you an unsafe one. And the iceberg isn't a fixed size, because it's attached to your release cycle. We'll come back to that.

Fig. 2 — The iceberg under the chatbot

There are six layers. For each we'll say the same two things: what a framework gives you, and what you still build. We mean “framework” kindly. An AI SDK, a component kit, an agent toolkit: these are good software that does what it claims, and the line between the two halves of each layer is exactly where the claims stop.

Layer 1: identity, or whose hands these are

What a framework gives you: a clean way to define tools the model can call, pass arguments, and get results back. Many let you attach headers to the outgoing request. What you still build: the answer to a deceptively small question. When the copilot acts, who is it?

There are three common answers. It can be a service account with broad access, which is quick to set up and means the copilot can do things the user can't, so you are one bug or one cleverly worded message away from showing a customer someone else's data. It can be a token your backend mints for each user, which works, and also means you've built a second way of authenticating that now has to be maintained, rotated and explained to auditors. Or it can be the signed-in user's own session: the copilot does what that person could do by clicking, and nothing more, and your API's existing checks stay the only checks.

We like the third, and its mechanical consequence is that the action has to run where the session lives, which is the user's browser. The model decides to call an action, the request travels to the open page, the page sends it to your API with the user's own cookies or token, and the result goes back to the model. Your session credentials never pass through the assistant's backend. Your API goes on enforcing authorization exactly as it did last week, and nobody has to write a second policy.

There's a second half that people skip, which is knowing who is in the conversation at all. That takes a short-lived token your server signs to say “this is user 42,” a way for the page to fetch a fresh one when the old one expires, and a plan for the awkward moments. The one nobody demos is sign-out. When one person signs out and another signs in on the same laptop, the open conversation, the history list and any pending prompts from the first person have to vanish at once. That sounds like an edge case until you remember that shared office computers exist.

Layer 2: permissions and a human yes

What a framework gives you: tool definitions and, in some kits, a confirmation hook. What you still build: a decision about every single action, and a place for a human to make it.

A filter rename and a refund don't belong in the same bucket. A workable scheme gives every action one of three dispositions: allowed outright, allowed only after the person approves that specific call, or denied. Approvals then need an interface that shows what is being approved and with which arguments, a record of who said yes, and a rule that an action waiting for approval does not run in the meantime, not even partly.

The distinction that matters most is between instructing and withholding. Telling a model in its prompt not to issue refunds is a sentence. Not giving it the refund tool is a fact about the system. Prompts are persuasion; tool lists are architecture. So a denied action shouldn't be in the model's tool list at all, and an ask-first action should wait for its yes before it goes anywhere near the user's browser.

Two smaller rules save you later. Grant actions one at a time, by name, so the list stays short enough for a person to read in a minute. And remember that each control answers exactly one question: an approval doesn't override your API's permissions, and a permitted action isn't automatically the right action for what the user asked. (Teams that build one control and assume it answers all of them tend to find out at a bad time.)

Notice where the refund question ends up. If the approver is the person in front of the screen, the approval arrives in seconds. If it's a manager, it arrives whenever the manager next opens their inbox. Which brings us to the layer that sounds boring and isn't.

Layer 3: durable turns

What a framework gives you: a loop. The model thinks, calls a tool, reads the result, thinks again and streams the answer, usually inside a single request. What you still build: everything that happens when the real world interrupts the loop, which it will.

Call one trip around the loop a round, and the whole task a turn. A turn can outlast an HTTP request, a deploy, the user's patience and, if it's waiting on a manager, a weekend. So it has to be a process that doesn't depend on the page that started it. Three situations make the point.

The tab closes: the turn should carry on without the page, and nothing short of the user pressing Stop should end it. The server restarts: progress has to be written down as the turn goes, so another machine can carry on from the last thing written without resending the message or repeating an action. The approval arrives on Monday: the turn has to wait without holding a machine, and wake up under the rules it started with.

Then there's the case that looks like it should have a clever solution and doesn't. The page closes in the middle of an action request. Did the refund go through? You can't retry it, because if it did go through you've refunded twice, and you can't assume it worked. The defensible move is to say so: report the outcome as unknown and have the assistant ask the user to look. Exactly-once delivery is a lovely idea. An honest shrug is what ships.

A framework can't hand you this, because it doesn't own the process. The loop lives in whichever server happened to receive the request, and when that server goes away, so does the loop. Durable turns mean building (or adopting) a runtime, and a runtime means somebody is on call.

Layer 4: knowledge that drifts from the code

What a framework gives you: retrieval. Chunk your docs, embed them, search them, put the best passages in the prompt. What you still build: whatever keeps those docs true.

Go back to the customer who was told to open “Billing contacts.” Retrieval worked perfectly. It found the help article and quoted it accurately, and the article was accurate on the day someone wrote it. Docs are a snapshot taken by a person at one moment, and your product keeps shipping afterward. A copilot makes the gap worse than a help center does, because a help center at least shows a date and a screenshot, while a copilot simply says it, fluently, in the present tense.

The fix is a loop rather than a feature. The knowledge has to be tied to the code it describes, re-checked whenever that code changes, and corrected by a person who reviews the proposed change, because silently rewriting what your assistant tells customers is a different kind of risk. The checkable claims (a setting's name, a limit, a default) can be checked. The rest, such as pricing policy or the reason a rule exists, still needs a human, and a system that pretends otherwise is just drifting with more confidence.

This is the layer that never finishes. If you've built the loop, each change costs a little. If you haven't, each change costs a customer ticket.

Layer 5: model routing and fallbacks

What a framework gives you: one interface over many providers, so swapping a model is a one-line change. What you still build: the policy for when to swap, and what happens when the swap lands at the worst moment.

Providers get throttled, overloaded and occasionally just go down, and they don't check your launch calendar first. So you need an ordered list: a default answering model, then fallbacks tried in turn. You also need to decide what counts as a reason to fall over. Outages, obviously. But also the quieter case where the conversation has grown past what the next model can hold. (That one is rarer than an outage, and more fun to debug.)

The case that breaks naive designs is failure mid-stream. The user is already reading the first half of a sentence when the provider drops. You have two options and both are bad: leave the half-sentence on screen and append a different model's answer to it, which produces nonsense, or show an error. The better move is to take back what the user saw, run the round again on the next model, and keep only the second answer in the record. The fallback then holds for the rest of that message, so the user isn't bounced between models mid-thought.

Smaller things pile up here too. Models support different levels of reasoning, so a fallback can't simply inherit the primary's settings, since it may not offer that level. Long conversations outgrow context windows, so something has to fold old messages into notes while keeping the originals readable. None of this is glamorous. It's the difference between a copilot that has a bad afternoon whenever a provider does, and one that doesn't.

Layer 6: the UI people actually need

What a framework gives you: a message list, a composer, streaming text and, in the good kits, hooks to build the rest. What you still build: the rest, which is longer than it looks.

Real users want history, meaning conversations they can find again, which ties conversations to identity (and means anonymous visitors get no history, which someone has to decide and say out loud). Somebody will drag a screenshot into the box, so you need attachments. Somebody will want the assistant in a drawer on one screen, a command palette on another and a full page on a help route, and they'll expect one conversation across all of them, so a reply streaming into the drawer should be streaming into the page too. A reload in the middle of a reply should replay the conversation from the record instead of dropping it.

Then there's the activity inside the conversation itself. When the assistant calls an action, users want a visible line saying that it did, what it called and what came back. Approval prompts need a design. Errors need wording. And all of it has to match your product's look, on mobile, with a keyboard, in dark mode. Nobody puts the approval dialog in a launch screenshot. It's still the screen customers will lean on hardest when they're nervous, and they will be nervous.

The maintenance bill

We could give you a number for what all this costs. We won't, partly because any number would be invented, and mostly because it depends on a variable we can't see: how often your product changes. So let's reason it through.

Every one of the six layers is coupled to something that moves. Identity follows your auth system, which changes the day you add single sign-on or rework sessions. Permissions follow your API, so every new endpoint is a fresh decision about whether the copilot may call it and whether a human must approve it. Durable turns sit on your infrastructure and feel every deploy, restart and scaling change. Knowledge has to keep up with every release you ship. Model routing is at the mercy of providers, who deprecate models, ship new ones and change their limits on their own calendar without consulting you. And the UI trails your design system, your browsers, and whatever your users currently believe chat should feel like.

The bill therefore has two terms: the number of layers, and the rate at which each layer's neighbor changes. Neither shrinks when you succeed. Shipping faster, which is presumably the goal, makes the bill larger. That's what a weekend demo can't show you, because a demo is a snapshot and the bill is a rate.

Fig. 3 — One release, six layers that need to hear about it

If you'd like something more concrete than reasoning, run an exercise. Open your product's changelog for the last month and write next to every entry which of the six layers it touches. Renamed a setting: knowledge. Added an endpoint: permissions. Moved to a new login provider: identity. Then count the entries that touch none of them. We'd be surprised if that's most of the list.

The other half of the bill is a person. Somebody owns each layer. Somebody gets paged when turns stop resuming, or when the fallback list turns out to be one provider listed three times. If you can name those people, that's a good sign. If the name you came up with is “whoever built the demo,” that's the finding.

The nahhh section

You've been drafting objections since the refund. Here are the three best.

Nahhh #1: “We'll just use a framework. It's open source.” Concede the true part first, because it's a big part. If the assistant is the product you sell, if your policies rule out someone else's runtime, if you already run an agent backend and only need a front end, or if you have people who want to own the runtime and the time to do it, a component kit or an agent toolkit is the right call, and a good one will save you months on the layers it covers. If that describes you, the build is yours to enjoy. Where the objection goes wrong is the word “open.” Open source makes the first line of code free. It doesn't make the maintenance free, and it doesn't put anyone on call. So run the test from the last section: for each of the six layers, can you name its owner? If yes, build it, happily. If you're naming people who don't know yet, that's your answer.

Nahhh #2: “Our use case is simple.” Sometimes it is, and it matters. An assistant that only answers questions from public docs has a much smaller iceberg: no actions means no acting as the user, no approvals, and no turn that has to wait for a manager. If that's what you're building, a framework plus decent retrieval may be most of the job. But look at what remains. The knowledge still drifts, the model provider still has bad afternoons, and the UI is still longer than it looks. And “simple” is usually a prediction about the roadmap rather than a description of it. It tends to expire the first time a customer asks, “can it just do it for me?”

Nahhh #3: “Buying means lock-in.” This one is half right, which is the most annoying way to be right. Whatever you buy has a shape, and moving off it costs something. But look at what is actually yours in that arrangement: your docs are text, your actions are descriptions of your own API, your prompts are prose. So put the real questions to any vendor: what leaves with you, in what format, and on what terms? (Building has lock-in too. It's just one person long.)

When the assistant takes actions, the iceberg becomes the product

Everything above applies to a copilot that answers questions. Now change one verb. Let the assistant do things.

With an answers-only assistant, the iceberg is infrastructure around a feature. If something breaks, a user gets a wrong answer and rolls their eyes. Once the assistant acts, it stops being a feature and becomes a way of operating your product, a second way in beside the screens. At that point the layers stop being plumbing. Identity decides whether your customers' data stays theirs. A refund either needs a person or it doesn't, and permissions are where that gets settled. Durable turns decide whether a task is ever left half done. Knowledge is the assistant telling users the truth about the thing they're paying for. Your users won't experience any of that as plumbing. They'll experience it as whether they trust your product.

That's the escalation, and it's why we think “should we build or buy a copilot?” is slightly the wrong question. The better one is where you want your team's attention to go. The model call and the chat panel are close to commodity. The six layers are slow, careful, mostly invisible work that rarely makes your product different from anyone else's. Build the ones that make you different, and be honest with yourself about the others.

Fig. 4 — Same tip, different berg: answers versus actions

Which leaves the team from the demo sitting with the first question: whose permissions does it call the API with? It's a good question, and it's only the first. The more useful one is which of the other five they'll hear about next, and from whom: a customer, a security review, or a very long weekend.

Teaflask is our attempt at shipping the part under the water as a product: it acts as the signed-in user, pauses consequential actions for a human yes, keeps turns alive across closed tabs and restarts, and re-checks what it knows against your code. If you'd rather build, the mechanics are written down in our architecture docs and the overview is on How it works, and either one works as a checklist for your own six layers.