> > > > > > >
AI Frontier Post
A sleek AI chat interface floating in dark space, message bubbles and an editable document side panel connected by swirling lines
Whirl is the full production code behind whirl.chat, published under the MIT license — illustration generated for this article.

Every good AI chat app eventually asks for the same ransom: your chat history on someone else's servers, one vendor's models, and a subscription that climbs with your team. Whirl (whirlchat/whirl, MIT) is a different deal — the full production code behind whirl.chat, published in the open: every top model in one conversation through OpenRouter, documents and charts that stay editable in a side panel, your own tools wired in over MCP, and long-term memory that survives between chats. This walkthrough takes it from a fresh clone to a running instance on your machine in about half an hour, then shows you how to take it to production.

Under the hood it is a Bun monorepo with a Next.js 16 front end (React 19, Tailwind v4) and a Convex backend that owns the database, the functions, and the AI pipeline. Sign-in comes from Clerk; every model call goes out through OpenRouter via the AI SDK. The design you should notice is the optionality: beyond those three, everything switches on with its own key — web search, memory, billing, analytics, tracing, email — and the features each key powers hide themselves until it is set.

What you’ll need

Step 1 — clone and install

git clone https://github.com/whirlchat/whirl.git
cd whirl
bun install

That pulls the whole monorepo: the web app in apps/v2, the admin console in apps/console, an Expo mobile app, the Convex backend in packages/backend, and docs. Take a minute in docs/self-hosting.md — it is the source of truth if anything here drifts.

Step 2 — set up Clerk

  1. Create an application in the Clerk dashboard and turn on the sign-in methods you want (email, Google, and so on).
  2. Open Configure → JWT templates, choose New template → Convex, and keep the name exactly convex. The backend only accepts tokens from a template with exactly that name — this is the single most common self-hosting failure, so double-check the spelling.
  3. Note three values: the Frontend API URL (looks like https://your-instance.clerk.accounts.dev), the publishable key (pk_test_...), and the secret key (sk_test_...).

Step 3 — push the backend to Convex

cd packages/backend
bunx convex dev

The first run logs you in and creates a new project plus a personal dev deployment. Then the first push stops with an error about CLERK_JWT_ISSUER_DOMAIN. That is expected: the backend cannot verify sign-ins until it knows your Clerk instance. Leave that terminal open, and in a second one set the required variables:

cd packages/backend
bunx convex env set CLERK_JWT_ISSUER_DOMAIN https://your-instance.clerk.accounts.dev
bunx convex env set OPENROUTER_API_KEY sk-or-v1-...
bunx convex env set SITE_URL http://localhost:3000

If you plan to connect integrations over MCP later, also set an encryption key for their stored credentials now:

bunx convex env set MCP_ENCRYPTION_KEY "$(openssl rand -base64 32)"

Then restart bunx convex dev in the first terminal. It pushes the backend and keeps watching packages/backend/convex for changes — leave it running. It also writes your deployment URLs to packages/backend/.env.local; you will need both in the next step:

Step 4 — configure the web app

cd apps/v2
cp .env.example .env.local

Fill in the required block:

NEXT_PUBLIC_CONVEX_URL=https://<name>.convex.cloud
NEXT_PUBLIC_CONVEX_SITE_URL=https://<name>.convex.site
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

Step 5 — run everything

From the repository root:

bun run dev

That starts three things side by side: the web app on localhost:3000, the admin console on localhost:3001, and the Convex watcher pushing your backend. Prefer separate terminals? Use bun run dev:v2, bun run dev:console, and bun run dev:backend. Sign in, send a message, and you are up.

Three glowing service nodes — a database, an identity key, and a model router — linked to a home server
Three managed services power it: Convex for the backend, Clerk for sign-in, OpenRouter for every model call. Illustration generated for this article.

Step 6 — switch on the extras

Each optional service activates as soon as its key is set, and its features stay hidden until then:

# on Convex
bunx convex env set EXA_API_KEY ...          # live web search + page reading
bunx convex env set SUPERMEMORY_API_KEY ... # long-term memory
bunx convex env set AUTUMN_SECRET_KEY ...   # plans and billing

The billing one deserves a pause. Without AUTUMN_SECRET_KEY, every signed-in user gets every feature and nothing is metered. For a personal or team instance that is exactly what you want — and every turn still records its provider cost, so the usage tab in settings stays accurate. But do not expose the instance to the open internet in that state unless you are comfortable buying tokens for strangers.

Modular glowing blocks — a wrench, a memory chip, a lock — snapping into a luminous AI pipeline
Optional extras — web search, memory, MCP integrations, billing — switch on with one key each and hide until then. Illustration generated for this article.

Step 7 — make yourself admin

Admins manage the model catalog, integrations, and skills from the console (apps/console):

  1. Make sure your Clerk convex JWT template includes the role claim from step 2: { "role": "{{user.public_metadata.role}}" }.
  2. In the Clerk dashboard, open your user and set public metadata to { "role": "admin" }.
  3. Sign out and back in so your token picks up the claim.
  4. Set up the console environment (cd apps/console && cp .env.example .env.local — same Convex URL and Clerk key as the web app) and open localhost:3001.

Step 8 — deploy to production

Backend first — create a production deployment and give it the same variables as dev, with the --prod flag:

cd packages/backend
bunx convex deploy
bunx convex env set --prod CLERK_JWT_ISSUER_DOMAIN https://clerk.your-domain
bunx convex env set --prod OPENROUTER_API_KEY sk-or-v1-...
bunx convex env set --prod SITE_URL https://your-domain

Use your Clerk production instance here (a different URL from dev) and add the same convex JWT template to it. For deploys on every push to main, add a CONVEX_DEPLOY_KEY (Convex dashboard → Settings → Deploy keys) to your GitHub repo — the included workflow .github/workflows/deploy-convex.yml picks it up automatically. The web app runs on any Next.js host: on Vercel, import the repo with the root directory set to apps/v2 and add the variables from apps/v2/.env.example with your production values.

What you built

Your own AI chat app, with a feature set that reads like a paid product’s changelog: every top model in one conversation with adjustable thinking levels; living artifacts — documents, charts, full interactive pages — that stay editable in a side panel and can be shared by link; MCP integrations with OAuth plus installable skills that teach the app new tricks; long-term memory that carries preferences and projects across chats; live web search for answers grounded in today’s web; and locked chats, encrypted on your device with a password the server never sees, answered only by zero-retention models. Incognito mode, message queueing, voice input, image generation, and file attachments are all in the box, and branding lives in one file — apps/v2/lib/site.ts — plus APP_NAME in the backend, which is the name OpenRouter shows for your traffic.

Honest limitations

Still: there is a real gap between a ChatGPT subscription and a chat app you can read, fork, and run on your own terms — with your tools, your memory, and your models. Whirl closes it in an afternoon, and the code stays yours either way. That is worth the half hour.