Tip
Try it live at oss.chat — no sign-up required, you get a guest budget and can start chatting right away. Then take the guided tour: a scroll-through of how the whole system works, from prompt to durable stream.
A local-first, multi-model AI chat app — and a working demonstration of how Prisma Streams, Prisma Postgres, and Prisma Next fit together in a real application.
Every assistant token is appended to a durable stream before the browser renders it. Close the tab mid-answer, reopen it, and the response replays and finishes exactly where it left off. No message is ever lost to a dropped connection.
- Prisma Streams as the system of record for chat messages: an append-only event log per user, with a routing key per chat. Appends are durable before delivery; reads resume from any offset.
- Prisma Postgres (via
prisma dev) for everything relational: users, sessions, chat metadata, and usage accounting — fully local. - Prisma Next as the typed data layer: the contract in
src/prisma/contract.prismadrives an end-to-end typed client, sharing onepg.Poolwith Better Auth.
- Streaming chat with any model on OpenRouter — text, vision, and image generation; switch models mid-conversation; each message records which model wrote it
- Durable, resumable streams: refresh, reconnect, or restart the server without losing tokens
- Anonymous guest sessions to try it instantly; sign up with email, GitHub, or Google and your guest chats come with you
- Credit-based billing: $2.00 free on signup, Stripe top-ups from $5 to $100 with a transparent 10% fee, and a free $0.50 drip after a month at zero
- Usage metering in micro-USD with a per-chat, per-model cost breakdown
- Markdown rendering, message permalinks, chat search, rename, and delete
- Image and audio models: attach, paste, or drop images and audio, dictate voice notes, and hear spoken replies live — originals in R2, thumbnails and transcripts in the durable log
- A public /stats page of anonymous usage aggregates — counts and token sums only, drawn with dependency-free SVG charts
- One process, no build step: Bun serves the API, the SSE proxy, and the React client
Browser (React + TanStack DB)
│ fetch /api/* ▲ SSE /api/chats/:id/events
▼ │
Bun server ── Better Auth (sessions, guests)
│
├── Prisma Next ──► Prisma Postgres chats, users, usage
│
├── Prisma Streams durable message events
│ one stream per user, routing key per chat
│
└── OpenRouter model streaming (only external call)
Sending a message appends a durable message.created event, then streams the model's reply as message.delta events into the same stream. The browser consumes them over an authenticated SSE proxy and folds them into messages with the same pure function the server uses for history replay (src/shared/messages.ts).
The full design is written up in docs/architecture.md.
The whole app is ~5,500 lines, and the durable-streaming core is much smaller than that. Reading these files in order tells the entire story:
| File | What it shows | |
|---|---|---|
| 1 | src/prisma/contract.prisma |
The Prisma Next contract: users, sessions, chats, usage — everything relational, fully typed end to end |
| 2 | src/server/streams.ts |
The Prisma Streams client: one append-only stream per user, one routing key per chat, reads resumable from any offset |
| 3 | src/server/routes/messages.ts |
The core path: append the user's message durably, stream model deltas into the same log, tail it over SSE |
| 4 | src/shared/messages.ts |
The materializer that folds the event log into messages — shared by server-side replay and the live client feed, so both always agree |
| 5 | src/client/stream.ts |
The client side of resumability: consume SSE, fold events, reconnect from the last offset |
| 6 | src/client/db.ts |
UI state as TanStack DB collections over the API |
| 7 | src/streams-app/index.ts |
The deployable Streams service: @prisma/streams-server with R2 as the durable tier, in ~30 lines of deployment defaults |
Everything else is ordinary app code: route handlers grouped by domain in
src/server/routes/, React components in
src/client/components/, and billing/auth as
supporting features around the core.
You need Bun ≥ 1.2 and an OpenRouter API key.
# 1. Install dependencies
bun install
# 2. Start local Prisma Postgres + Prisma Streams
bun run db:dev
# 3. Configure the environment
cp .env.example .env
# – set DATABASE_URL and STREAMS_URL to the URLs printed by:
# DATABASE_URL=... bunx prisma dev ls
# – set OPENROUTER_API_KEY and a random BETTER_AUTH_SECRET
# 4. Create the database tables
bun run db:init
# 5. Run the app
bun run devOpen http://localhost:3000 — you'll be signed in as a guest automatically and can start chatting.
The live instance at oss.chat runs on Prisma Compute as two apps in one project: the chat server and the durable Streams service it talks to. Deploying your own takes a few minutes with the Prisma CLI.
# 1. Sign in and create a project (this also provisions a Prisma Postgres database)
bunx @prisma/cli@latest auth login
bunx @prisma/cli@latest project create my-open-chat
# 2. Create the tables in the project's primary database
bunx @prisma/cli@latest database connection create <database-id> # prints DATABASE_URL
DATABASE_URL=<that-url> bun run db:init
# 3. Deploy the Streams service (pick any long random key). It runs
# @prisma/streams-server — the full Prisma Streams runtime — with R2 as
# the durable tier: segments upload continuously, and a fresh instance
# rehydrates from the bucket, so chat history survives the platform
# replacing the instance (its local disk is ephemeral).
bunx @prisma/cli@latest app deploy --app Streams \
--framework bun --entry src/streams-app/index.ts --http-port 8080 \
--env STREAMS_API_KEY=<random-key> \
--env DURABLE_STREAMS_R2_BUCKET=<bucket-name> \
--env DURABLE_STREAMS_R2_ACCOUNT_ID=<cloudflare-account-id> \
--env DURABLE_STREAMS_R2_ACCESS_KEY_ID=<r2-access-key> \
--env DURABLE_STREAMS_R2_SECRET_ACCESS_KEY=<r2-secret> \
--no-db --prod --yes
# 4. Deploy the chat app, pointing it at the database and the Streams URL from step 3
bunx @prisma/cli@latest app deploy --app open-chat \
--framework bun --entry src/start.ts --http-port 3000 \
--env DATABASE_URL=<that-url> \
--env STREAMS_URL=<streams-app-url> \
--env STREAMS_API_KEY=<random-key> \
--env BETTER_AUTH_SECRET=<random-secret> \
--env OPENROUTER_API_KEY=<your-openrouter-key> \
--env APP_ORIGIN=<chat-app-url> \
--no-db --prod --yesThat's it — the CLI builds locally, uploads, and the deployment is live in seconds. Secrets live only in Compute's env config, never in the repo. (On the very first deploy you don't know the app URL yet: deploy once, then set APP_ORIGIN to the printed URL and deploy again. Subsequent deploys keep their env vars.)
| Path | What lives there |
|---|---|
src/server/ |
Bun HTTP server; route handlers grouped by domain in routes/, plus the Streams client, OpenRouter client, usage budgets, and auth |
src/client/ |
React UI: a thin App.tsx gate, views in components/, state as TanStack DB collections in db.ts |
src/shared/ |
Zod contracts and the event-log → message materializer, shared by both sides |
src/prisma/ |
Prisma Next contract and the typed database client |
src/streams-app/ |
The standalone Streams service deployed next to the app |
docs/ |
Architecture, feature checklist, design system, verification log; brand assets in docs/logo/ |
| Command | Purpose |
|---|---|
bun run dev |
App server with hot reload |
bun run db:dev |
Local Prisma Postgres + Streams via prisma dev |
bun run db:init |
Create tables from the Prisma Next contract |
bun run db:generate |
Re-emit contract types after editing contract.prisma |
bun test / bun run typecheck |
Tests and strict TypeScript |
- oss.chat/tour — the guided tour: an animated walk through the whole flow, one section per piece of the stack
docs/architecture.md— the stream-per-user / routing-key-per-chat pattern, data ownership, and the durable streaming pathdocs/features.md— every user flow, written as a verification checklistprisma-next.md— how the Prisma Next contract workflow operates in this repo
