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.
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.
Warning
The Supabase project must sign tokens with asymmetric keys (Authentication → JWT Keys → ES256/RS256 current key). Projects created since 2025 already do; older projects on the legacy HS256 secret need to rotate to a signing key first. mordn deliberately cannot verify HS256 — that would require holding your secret.
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.
Security
The browser never says who it is. It can only present a token Supabase signed, and a
token mordn cannot verify is a 401 — not an anonymous fallback. The publishable key is
public by design: it selects the agent and only works from the origins you allowlisted.
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.
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/…/bootstrapis200when signed in and401when signed out (or200with ananon: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_REFRESHEDmust not change it. - No
mck_key anywhere in the repo. Onlypk_live_…ships.
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.