# chat.spboucher.ai ![Status](https://img.shields.io/badge/status-live-3FB6A8) ![Models](https://img.shields.io/badge/models-409%2B-3FB6A8) ![Next.js](https://img.shields.io/badge/Next.js-16-0E1116) ![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6) ![SQLite](https://img.shields.io/badge/SQLite-WAL-E0A458) ![Gateway](https://img.shields.io/badge/gateway-OpenRouter-8B5CF6) ![PWA](https://img.shields.io/badge/PWA-installable-3FB6A8) ![Node](https://img.shields.io/badge/deploy-m4m64a%20%C2%B7%20ngrok-151A22) ![License](https://img.shields.io/badge/license-private-2A3240) > **Author:** Simon-Pierre Boucher > **Contact:** [contact@spboucher.ai](mailto:contact@spboucher.ai) > **Production:** [https://chat.spboucher.ai](https://chat.spboucher.ai) A private, universal chat interface over the entire **OpenRouter** ecosystem — one login, one server-side API key, 400+ models behind a single catalog, full conversation persistence with branching and per-message model attribution. Designed mobile-first as an installable PWA: a personal ChatGPT/Claude-class product, not an API demo. --- ## Highlights - **One gateway, hundreds of models** — the catalog is synced dynamically from OpenRouter into SQLite (never hand-maintained); removed models are marked `Unavailable`, historical conversations stay intact forever. - **Real streaming** — normalized SSE event protocol (`generation.start`, `content.delta`, `reasoning.delta`, `usage`, …) with 15 s keep-alive pings that survive ngrok and mobile radio sleep. Stop truly aborts the upstream OpenRouter stream. - **Branching & regeneration** — regenerating never destroys an answer; siblings live side by side with ‹ 1/2 › navigation, so the same prompt can be compared across models. - **Generation state machine** — `queued → starting → streaming → completed | cancelled | failed`, persisted per generation; a dropped mobile connection resyncs from the server. - **Usage & cost tracking** — prompt/completion/reasoning/cached tokens and reported cost per generation, with a Today / 7d / 30d / All-time dashboard broken down by model. - **Instrument Panel design** — ink/graphite surfaces, one teal accent for "alive", one amber accent for "cost", mono type for model IDs and meters, and the signature **Model Rail** cartridge attached to the composer (provider glyph · mono model ID · live context meter · price-per-1M chip). - **Security posture for a public tunnel** — argon2id single-user auth, DB-backed sessions, aggressive login rate limiting, `Secure`/`HttpOnly`/`SameSite` cookies, CSP + security headers, zero unauthenticated API surface. ## Architecture ``` Browser (phone or desktop, PWA) │ POST /api/chat (SSE) ▼ Next.js 16 backend — node m4m64a (Apple Silicon, macOS) │ src/lib/openrouter/ (single client module, normalized errors/streaming) ▼ OpenRouter API ──► Anthropic · OpenAI · Google · Meta · Qwen · DeepSeek · Mistral · … ``` - **Runtime:** single Node.js process (API + UI), port 3000, exposed via ngrok at `chat.spboucher.ai` - **Database:** SQLite (better-sqlite3, WAL) — conversations, message tree, generations, usage, model catalog cache, sessions - **Supervision:** three launchd agents (`app`, `ngrok`, nightly `backup` at 03:30 with 14-day retention) ## Local development ```bash cp .env.example .env # fill in OPENROUTER_API_KEY npm install npm run set-password [password] # generates a strong one if omitted npm run dev ``` ## Deployment (m4m64a) ```bash rsync -az --delete --exclude node_modules --exclude .next --exclude data --exclude .env \ ~/Desktop/chat-spboucher/ M4M64a:apps/chat.spboucher.ai/ ssh M4M64a '~/apps/chat.spboucher.ai/ops/deploy.sh' ``` | launchd agent | Role | |---|---| | `ai.spboucher.chat.app` | `npm run start` on port 3000, KeepAlive | | `ai.spboucher.chat.ngrok` | `ngrok http --url=chat.spboucher.ai 3000`, KeepAlive | | `ai.spboucher.chat.backup` | nightly SQLite backup 03:30, 14-day retention | ## API surface All routes require a session cookie except `POST /api/auth/login`. | Endpoint | Purpose | |---|---| | `POST /api/chat` | send a message or regenerate (`{regenerateOf}`) — returns a normalized SSE stream | | `POST /api/generations/:id/cancel` | abort the upstream OpenRouter generation | | `GET /api/models` · `POST /api/models/sync` | cached catalog · manual refresh | | `POST /api/models/prefs` | favorites / pinned models | | `GET/PATCH/DELETE /api/conversations[/:id]` | conversation CRUD, branch leaf selection | | `GET /api/usage?period=today\|7d\|30d\|all` | usage & cost dashboard data | ## License Private software — © Simon-Pierre Boucher. All rights reserved. --- **Simon-Pierre Boucher** · [contact@spboucher.ai](mailto:contact@spboucher.ai) · [chat.spboucher.ai](https://chat.spboucher.ai)