TinyStore

Start / The chat we build

The chat we build

The chapters of Building the chat write one application: the backend of an AI chat, where people sign in, send messages and get answers from a language model. This chapter shows the whole plan: what the chat does, which engine handles each part, and which chapter adds it.

Why a chat

A chat is small enough to read in an afternoon, and it meets most of the problems a real backend meets:

  • A request arrives twice. A phone on a bad network sends a message, times out and sends it again. The chat must not answer it twice.
  • Work is slow and fails. A model takes seconds to answer and sometimes fails. A request cannot wait for it, and a failed call must be retried.
  • A process dies at a bad moment. A deploy restarts the server while a reply is half written. The reply must still arrive.
  • Users share limits. A free plan allows 100 messages per 5 hours and 300 per week, across every device of the user.
  • Several tabs watch one chat. A message sent from a phone appears in the laptop's open tab at once.

Each of these is usually a service, a table and some careful code. In the chat, each is a few lines on top of an engine.

What the chat does

Route or processWhat it doesEngines
POST /sign-insends a one-time code, then starts a sessionKV
POST /chats/:id/messagesaccepts a message once, checks the plan's limitsSQL, KV, Jobs
the reply workerasks the model, keeps its finished steps, saves itJobs, SQL, Metrics
GET /chats/:id/eventsstreams new messages and the reply's progress to tabsChannels, Jobs
POST /chats/:id/attachmentsstores an uploaded file and serves it backBlobs
GET /search?q=finds old messages by words and by meaningSearch
the summary jobwrites one summary per chat at a timeJobs, Locks
the model's settingsits name, its limits and a switch to turn it offConfig
everythinglogs each request, times each model callRecords, Metrics
at nightbacks up the whole storeBackup

A message travels through the chat like this:

browseroncea request sent twice gets one answerquota100 per 5 hours, 300 per weeksql and jobs, one committhe message and its reply jobreply workerretried, its steps keptsqlinsert the replychannelstell every open tabPOST /chats/7/messagesthe jobthe answerserver-sent events
Tap to enlarge

The chapters

ChapterThe chat gains
Messagesa database with migrations, the messages table, writes and queries
Sessions and limitssign-in codes, sessions that expire when idle, quotas, requests sent twice
Replies in the backgrounda queue in the chat's database, a worker, retries, steps, progress
Attachmentsuploads written whole or not at all, served with ranges, expiry
Finding old messagesan index of the messages table, by words, by vectors, with typos
Live updatesa new message in every open tab; one summary job per chat at a time
Settingsthe model's settings from a file and the environment, some changed live
What it is doinga logger, a trace per request, reading the logs back
How fast it istimers and counters, aggregates, the store's own metrics
Keeping ita backup while it runs, a restore, tests on a clock you move by hand

Every chapter starts from the chat as the previous one left it, adds one capability, and ends with a program that runs. You can stop after any chapter. The code is shown in TypeScript, Python, Go and Rust, and the four versions behave the same way, because they run the same core.

What it starts from

The chat opens one store and one database, as One directory, many engines showed:

import { open } from '@tinyshed/tinystore'

await using store = await open('./data')
const db = await store.database('chat', { migrations: './migrations' })
async with await tinystore.open("data") as store:
    db = await store.database("chat", migrations="migrations")
store, err := tinystore.Open(ctx, "data")
if err != nil {
	return err
}
defer store.Close()
db, err := sqldb.Open(ctx, store, "chat", sqldb.Migrations(os.DirFS("migrations")))
let store = Store::open("data", Options::default())?;
let db = store.database("chat").migrations("migrations").open()?;

Everything else opens from these two handles, as each chapter needs it.

What it will not need

At the end of the book the chat runs on one machine with no Redis, no queue service, no object storage, no log shipper and no metrics server. It also has no expires_at columns, no cleanup cron jobs, no status table for replies, no retry loops and no table of seen request ids. Each of those is a feature of an engine.

When the chat outgrows one machine, Beyond one machine moves it onto three: one that writes, and two in other regions that serve reads and pass their writes home.

See also

Edit this page on GitHub Updated 19 October 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results