Skip to content
mordn
Esc
↑↓navigate↵open⌘Jpreview
On this page

Quickstart: Vite + Supabase

Add a production chat agent to a Vite React app with no server. One component, a publishable key, and the Supabase session your app already has.

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.

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.

Create an account, add an agent, publish it, and copy a server key (mck_live_…). Use the key once, from your terminal, to enable hosted mode:

curl -X PUT https://api.mordn.com/v1/hosted/settings \
  -H "Authorization: Bearer mck_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "allowedOrigins": ["http://localhost:5173", "https://app.example.com"],
    "jwksUrl": "https://xyz.supabase.co/auth/v1/.well-known/jwks.json",
    "issuer": "https://xyz.supabase.co/auth/v1",
    "audience": "authenticated"
  }'

For a visitor-only agent (no Supabase Auth), send "anonymousEnabled": true and leave out jwksUrl, issuer, audience.

Copy settings.publishableKey from the response — it looks like pk_live_… and is safe to put in client code. The mck_live_… key stays in your terminal; it is never part of the app.

Install

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

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

Add the component

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.

Mount it

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

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.

Leave the layout as popup and render <Assistant /> once, near the root:

export default function App() {
  return (
    <>
      <YourApp />
      <Assistant />
    </>
  );
}

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.

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