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/reactAdd 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-widgetversion 0.23.0 or later,aiand@ai-sdk/react. Use our existing Supabase client and auth subscription withChatWidgetandpublishableKey="pk_live_…". Follow step 3: wait for initial auth, publish the latest session and a reactiveauthSessionKeyrevision together on login/logout/account switch, and havegetUserTokenread that session. Do not increment on same-userTOKEN_REFRESHEDor repeated same-userSIGNED_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.