---
title: "Quickstart: Vite + Supabase"
description: "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.

> **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](/docs/quickstart) mounts the handler in your
> own route instead.

1. **Create the agent in hosted mode**

    **With your coding agent**

    Connect the [mordn MCP server](/docs/mcp) 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.

    **By hand**

    [Create an account](https://mordn.com/sign-up), add an agent, publish it, and copy a server key
    (`mck_live_…`). Use the key **once, from your terminal**, to enable hosted mode:

    ```bash
    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.

2. **Install**

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

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

3. **Add the component**

    ```tsx title="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](/docs/hosted-mode#login-logout-and-account-switches).

    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.

4. **Mount it**

    **Full-page app**

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

    ```tsx title="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.

    **Popup**

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

    ```tsx title="src/App.tsx"
    export default function App() {
      return (
        <>
          <YourApp />
          <Assistant />
        </>
      );
    }
    ```

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

**[Hosted mode reference](/docs/hosted-mode)**

Every setting, every identity preset, and the server-vs-hosted comparison.

**[Knowledge / RAG](/docs/guides/knowledge)**

Ground the agent in your docs so it cites them.

**[Theming](/docs/guides/theming)**

Brand colours and the page layout.

**[MCP server](/docs/mcp)**

Let your coding agent do all of this for you.
