All posts

September 9, 2026 · 8 min read

Add live chat with an AI agent to a Next.js app in 10 minutes

A step-by-step walkthrough: install the widget in the App Router, identify signed-in users securely, hide it on admin routes, and let an AI answer from your docs before you wake up.

Aditya SinghFounder, SvellyoGuidesNext.jsAI agent

Most indie SaaS apps get live chat in one of two ways. Either you paste a vendor's script tag into layout.tsx and hope it does not fight your CSS, or you put it off for a year because every option seems to want a per-seat subscription and a week of configuration.

This guide takes the first path and fixes its problems. By the end you will have a chat widget in a Next.js App Router project that knows who your signed-in users are, stays off your admin pages, and answers questions from your own documentation before a human ever looks at the inbox. It takes about ten minutes of real work, most of which is copying two snippets.

What you will need

  • A Next.js 14 or newer project using the App Router. The Pages Router works too, but the snippets below assume app/layout.tsx.
  • A Svellyo workspace. The free plan covers 50 chats a month and does not need a card. Create one at svellyo.com/sign-up, name it after the product, and note the workspace id that starts with ws_ on the Installation page.
  • Five minutes of documentation you are willing to paste into a knowledge base. Your README, your pricing page and your FAQ are enough to start.

Step 1: install the SDK

npm i @svellyo/sdk

The package is small and has no runtime dependencies. It exports a React component that renders nothing itself. All it does is inject the widget script once and forward configuration to it, so it is safe in a server component tree and does not affect your bundle size in any meaningful way.

Step 2: render the widget once

Open your root layout and add the component at the end of body. It must live in the root layout, not on individual pages, because the widget keeps the conversation open across navigations.

// app/layout.tsx
import { SvellyoWidget } from "@svellyo/sdk/react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <SvellyoWidget workspaceId="ws_your_id" />
      </body>
    </html>
  );
}

Start the dev server and load any page. The launcher bubble appears in the bottom right within a second. If it does not, add http://localhost:3000 to Settings, General, Allowed origins in the dashboard. The widget refuses to run on domains you have not listed, which is what you want in production and a mild surprise in development.

Send yourself a message. It shows up in the inbox immediately. That is the whole install. Everything after this point is making it better.

Step 3: identify signed-in users

An anonymous "Visitor" in your inbox is fine for a marketing site and useless inside a product. When someone writes "my export is broken", you want their email, their plan and their account id next to the message, and you want their chat history to follow them from their laptop to their phone.

Pass a user prop from wherever you have the session. In a server component root layout, fetch the session and pass a plain object down.

// app/layout.tsx
import { SvellyoWidget } from "@svellyo/sdk/react";
import { auth } from "@/lib/auth";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const session = await auth();
  const user = session
    ? {
        userId: session.user.id,
        email: session.user.email,
        name: session.user.name,
        attributes: { plan: session.user.plan, signedUpAt: session.user.createdAt },
      }
    : null;
  return (
    <html lang="en">
      <body>
        {children}
        <SvellyoWidget workspaceId="ws_your_id" user={user} />
      </body>
    </html>
  );
}

Two details matter here. First, use a stable userId rather than only an email. People change emails, and you want the same contact either way. Second, pass null on logout. The component resets the visitor session when user goes from an object to null, so the next person on a shared machine does not see someone else's conversations.

Attributes are free-form. Anything you pass shows up in the contact panel in the inbox and, more usefully, in the context the AI agent sees. If a customer on the free plan asks about a feature that is Pro only, the agent can say so, because plan: "free" is right there.

Step 4: sign the identity so nobody can spoof it

Everything in Step 3 runs in the browser, which means a curious user could open the console and call identify with somebody else's email. On a marketing site that does not matter. Inside a product with chat history, it does.

Svellyo fixes this with a signed hash. On your server, compute an HMAC-SHA256 of the user id with your workspace's identity secret, and pass the result as hash.

// lib/svellyo.ts (server only)
import { createHmac } from "node:crypto";

export function svellyoHash(userId: string) {
  return createHmac("sha256", process.env.SVELLYO_IDENTITY_SECRET!).update(userId).digest("hex");
}
const user = session
  ? {
      userId: session.user.id,
      email: session.user.email,
      name: session.user.name,
      hash: svellyoHash(session.user.id),
    }
  : null;

Then turn on Require verified identity on the Installation page. From that moment, an identify call without a valid hash is ignored and the visitor stays anonymous. The secret never reaches the browser, and the hash is useless for any other user id.

Step 5: keep it off admin and checkout pages

You do not want a support bubble floating over your own admin dashboard or hovering next to a payment form. The lazy fix is to render the component conditionally per page, which breaks conversation continuity and puts the widget in the wrong layouts. The right fix is to keep one install in the root layout and tell it where not to appear.

<SvellyoWidget
  workspaceId="ws_your_id"
  user={user}
  excludePaths={["/admin/*", "/checkout", "/embed/**"]}
/>

The widget watches the History API, so it re-evaluates the list on every client-side navigation without a remount. /checkout is exact, /admin/* covers everything under /admin, and ** matches any depth. If you would rather change this without a deploy, the same list lives under Settings, Widget, Hide on these pages in the dashboard and applies to every install of that workspace.

For moments rather than routes, for example during a video call inside your app, there is Svellyo.hide() and Svellyo.show(), or the useSvellyo() hook if you prefer.

Step 6: teach the AI before you go to bed

At this point you have a good live chat. What makes it worth having is the part that answers while you are asleep.

Open Knowledge base in the dashboard. Create an article, paste your README or your FAQ, and publish it. The editor accepts Markdown on paste, so a .md file straight from your repo becomes formatted content with headings and code blocks intact. Do the same for your pricing page. If you have docs on a public site, add it as a website source under Sources and let the crawler index it.

Then open Settings, AI and write two paragraphs of instructions. Something like:

You are the support assistant for Acme. Be concise. Only answer from the knowledge base. If you are not sure, say so and offer to bring in a person. Never promise refunds or discounts.

Use the test chat on the same page to ask the questions your customers actually ask. Watch the confidence score and the cited source on each answer. When it cites the wrong article, split or retitle the article. When it has no article to cite, write one. Ten minutes of this is worth more than any prompt engineering.

What happens when the AI is not sure

This is the part that decides whether you can trust it. Svellyo's agent scores every reply. Below the threshold you set, it does not guess. It tells the visitor it is bringing in a person, moves the conversation to Needs human in your inbox, and pings you by email, Slack or Discord. If the visitor types "talk to a human" or taps the button in the widget, the same thing happens immediately.

When you reply, the AI steps aside for that conversation. When you are done, click Hand back to AI and it picks the thread back up with the full context. Nothing about this needs configuration beyond the threshold, and the default is conservative.

A note on performance

The widget is about 22 KB gzipped and loads asynchronously after your page. It runs inside a Shadow DOM, which means your global styles cannot leak into it and its styles cannot leak out. It sets no cookies. If you run Lighthouse before and after, the difference is within noise, and there is nothing to add to your cookie banner.

Where to go from here

You now have chat on every page of your app, signed-in users showing up by name, the widget staying out of your admin, and an AI answering from your own words. From here the useful additions are:

  • Suggested questions on the widget's home screen, pointed at the things your knowledge base answers best.
  • Business hours, so visitors see an honest "we will reply tomorrow" outside your day, with the AI still covering the night.
  • Slack or Discord alerts for escalations, so you do not have to keep the inbox open.
  • A second workspace for your second product. The same SDK, a different ws_ id, and both land in the same inbox on the same plan.

If you get stuck, the install docs cover every framework, and the chat in the corner of svellyo.com is, naturally, running the same thing.