> > > > > > >

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.
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.
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.https://your-instance.clerk.accounts.dev), the publishable key (pk_test_...), and the secret key (sk_test_...).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:
https://<name>.convex.cloudhttps://<name>.convex.sitecd 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_...
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.

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.

Admins manage the model catalog, integrations, and skills from the console (apps/console):
convex JWT template includes the role claim from step 2: { "role": "{{user.public_metadata.role}}" }.{ "role": "admin" }.cd apps/console && cp .env.example .env.local — same Convex URL and Clerk key as the web app) and open localhost:3001.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.
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.
docs/self-hosting.md before running it.OPENROUTER_API_KEY is not set; a signed-in-but-unauthenticated state means the JWT template is not named exactly convex; locked chats failing to start means NEXT_PUBLIC_CONVEX_SITE_URL is missing from the web app.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.