Skip to content

Quickstart: Vite + Supabase

Add a full AI assistant — streaming, per-user history, knowledge, memory — to a Vite React app that has no backend of its own. This is the shape Lovable, Bolt and most Supabase-backed SPAs ship in.

One component changes. There is no server route to write and no secret to store.

Note

Prerequisites: a Vite + React app, @supabase/supabase-js already configured (a supabase browser client), and users who sign in with Supabase Auth. No Supabase Auth yet? Skip getUserToken and use anonymous visitors — step 1 shows both.

Have a Next.js app? The Quickstart mounts the handler in your own route instead.

1

Create the agent in hosted mode

Connect the mordn MCP server to Claude Code, Cursor or Windsurf, then:

Add a mordn agent to this app in hosted mode. Origins: http://localhost:5173 and https://app.example.com. Supabase project: https://xyz.supabase.co. Full-page layout at /assistant, grounded in https://docs.example.com.

It creates and publishes the agent, enables hosted mode with the Supabase identity preset, and supplies the real publishable key. Compare generated code with step 3: a token getter alone is not a complete auth lifecycle integration. Wire the existing Supabase auth subscription before mounting the widget.

2

Install

The auth lifecycle API below requires @mordn/chat-widget 0.23.0 or later.

code
npm install '@mordn/chat-widget@^0.23.0' ai @ai-sdk/react
3

Add the component

code
src/components/Assistant.tsx
import { useEffect, useState } from 'react';
import type { Session } from '@supabase/supabase-js';
import { ChatWidget } from '@mordn/chat-widget';
import '@mordn/chat-widget/styles.css';
import { supabase } from '@/lib/supabase'; // your existing browser client
 
export function Assistant() {
  const [auth, setAuth] = useState<{
    session: Session | null;
    revision: number;
    ready: boolean;
  }>({ session: null, revision: 0, ready: false });
 
  useEffect(() => {
    // INITIAL_SESSION supplies the starting session. Keep this callback synchronous.
    const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
      setAuth((previous) => {
        // Local transition detection only; these ids are NOT sent to mordn.
        const changed = (previous.session?.user.id ?? null) !== (session?.user.id ?? null);
        return { session, revision: previous.revision + (changed ? 1 : 0), ready: true };
      });
    });
    return () => subscription.unsubscribe();
  }, []);
 
  if (!auth.ready) return null;
 
  return (
    <ChatWidget
      publishableKey="pk_live_…"
      authSessionKey={auth.revision}
      getUserToken={() => auth.session?.access_token ?? null}
    />
  );
}

getUserToken returns the Supabase access token — a JWT your project signed. The widget sends it as Authorization: Bearer …; mordn verifies the signature against your project's JWKS and binds the conversation to the token's sub. It is re-read every few minutes (every four minutes) for token maintenance. Changing the callback identity or re-rendering does not trigger token resolution.

The subscription publishes the session and authSessionKey revision together. It waits for INITIAL_SESSION before mounting, increments on login/logout/account switch, and keeps the revision stable for TOKEN_REFRESHED or repeated SIGNED_IN events for the same user. It still stores every refreshed token. Logout then login as the same user increments twice, even when React batches the state updates. The ids used for this local comparison are never sent as authoritative identity. Do not replace the revision with a token or a constant. If your existing auth provider hides intermediate logout transitions, give each new login a revision or unique session id of its own.

On a lifecycle change, the widget drops old auth/bootstrap/mounted chat state and waits for the latest getter before bootstrapping — it does not temporarily fall back to an anonymous visitor. If you already own the auth state in a provider, publish its session and revision there instead of installing a second auth source.

Alternatively, attach a ChatWidgetHandle ref and call resetAuth() from your existing auth transition handler after updating the getter's session source. Do not reset immediately after setting React state that the getter still closes over; prefer the reactive key shown here. Use one mechanism, never a render or token-refresh reset loop. See the hosted lifecycle reference.

Want signed-out visitors to get a chat too? Add the anonymous prop (and anonymousEnabled on the agent). Each browser gets its own history under an anon: id; when the visitor signs in, the lifecycle revision switches them to their signed-in scope. Reset does not erase persisted history/drafts; call clearChatStorage() on sign-out/account switch if your policy requires erasure (the anonymous visitor id is retained). Also clear/update your own conversationId, initialMessages, and any other user-specific props. Only the server-verified token authorizes access; neither the revision nor reset revokes tokens or replaces server authorization.

4

Mount it

Publish the agent with layout page (in the dashboard, or layout: "page" via MCP) and give it a route:

code
src/App.tsx
import { Routes, Route } from 'react-router-dom';
import { Assistant } from './components/Assistant';
 
export default function App() {
  return (
    <Routes>
      {/* … */}
      <Route path="/assistant" element={<Assistant />} />
    </Routes>
  );
}

The widget fills the viewport with a thread sidebar and a composer — a ChatGPT-style app for your product, without building one.

5

Send a message

npm run dev, sign in, open the assistant, type. Reload — the conversation is still there. Open a second browser profile: a different user, a different history.

Before you deploy

  • Your production origin is in allowedOrigins — otherwise the widget fails CORS in production while working locally.
  • In DevTools, the request to api.mordn.com/v1/hosted/…/bootstrap is 200 when signed in and 401 when signed out (or 200 with an anon: history if you enabled anonymous visitors).
  • Without a reload, test logout → login as the same user, then an account switch. The revision changes at each boundary and old mounted content disappears while the next token/bootstrap resolves. Same-user TOKEN_REFRESHED must not change it.
  • No mck_ key anywhere in the repo. Only pk_live_… ships.

Lovable, Bolt, v0

Paste this into the builder's chat once the agent exists:

Install @mordn/chat-widget version 0.23.0 or later, ai and @ai-sdk/react. Use our existing Supabase client and auth subscription with ChatWidget and publishableKey="pk_live_…". Follow step 3: wait for initial auth, publish the latest session and a reactive authSessionKey revision together on login/logout/account switch, and have getUserToken read that session. Do not increment on same-user TOKEN_REFRESHED or repeated same-user SIGNED_IN. Do not use the token, a constant, or a plain user id as the lifecycle key. Import @mordn/chat-widget/styles.css, mount at /assistant, and clear/update user-specific caller props on transitions. Do not add any API route or environment variable.

If the builder has the mordn MCP server connected, the shorter version is "add a mordn agent to this app in hosted mode" — it will create the agent and fill in the key itself.

Next steps