Chatbot Build
How was the docs chatbot implemented?
Docs Indexing
public: falsepages are still indexed —/docs/chatappliesFLAG_PUBLICat query time so a public build only retrievespublic: truechunks.- Inline user guide search and
/llms.txtkeep the existingflags.publicfilter and are separate from the chatbot index.
src/lib/docs/compose.ts with forIndex: true builds the text that later goes into docs_section:
- Reads the docs MDX files from a Git branch (
BRANCH, defaultmain). - Shared
_snippetsfiles are pasted into the page that imports them, so the index stores the full wording, not an import line. - Each chunk is marked
public: trueorfalsefrom the page frontmatter, so chat can filter later. - A page becomes one overview chunk plus one chunk per heading (the heading's
hrefmay include#anchor).
npm run sync-docs runs scripts/sync-docs.ts to embed that text and upsert it into docs_section:
- Gemini (
gemini-embedding-001, 768 dimensions,RETRIEVAL_DOCUMENT) turns chunks into vectors in batches of 20 for meaning search. - Each chunk is stored with a
content_hashof that text. A matching hash skips Gemini and the write. - Title, heading, and body also fill a weighted
search_vector(English full text, so "running" and "run" can match). A title match outranks a heading, and a heading outranks the body. - Rows are upserted with the Supabase
service_roleclient. Chunk ids that disappeared from the corpus are deleted. - Vectors already computed stay in
internal/supabot/embed-cache.json(gitignored), so a machine that embedded a chunk can skip Gemini when the row is missing. A merged docs change onmainruns the same script from GitHub Actions, which usescontent_hashbecause that cache file is not in the repo.
Chatbot Interface
Clicking the ask the documentation button (DocsChat) opens the DocsChatPanel. /docs/chat/route.ts:
- Restores message history it the browser has a
docs_chat_idcookie. (GET) - Looks up matching docs in
docs_section, then asks Gemini to write a summary from those excerpts only on Send. (POST) - Clears the message history by dropping the current
docs_chat_idcookie and the matching Supabase session when Clear Chat is clicked. (DELETE)
- Gemini (
gemini-embedding-001, 768 dimensions,RETRIEVAL_QUERY) turns the question into a vector so pages can match by meaning, not only by the same words. If embed quota is hit, keyword search still runs. - The same wording reuses
docs_query_embedding(scripts/supabot.sql). The row ID is a hash of the question, so the question text is not stored. The server instance also keeps vectors it has already loaded, and a trigger keeps the newest 200 rows. Row-level security is on with noanon/authenticatedpolicies./docs/chatreads and writes those rows with theservice_roleclient. - Postgres then runs two lookups — wording match (
search_docs_sections, ranked onsearch_vector) and meaning match (match_docs_sections, cosine distance). Those ranked lists are combined (RRF) in/docs/chatso a page that does well on either can still surface. - At most three pages are kept, and only if their combined score is close to the best hit. Weaker matches are dropped.
- Privacy Policy, Terms of Service, and Cucumber Tests are pushed down unless the question is actually about them.
- Short words such as
apiandaddare kept for matching (three characters or more). - If a heading on that page matches the question, that heading's excerpt is used instead of the page overview.
- When
FLAG_PUBLICis on, onlypublic: truepages are retrieved. That filter runs inside each lookup, before the row limit.
- Gemini (
gemini-3.6-flash) writes the reply from those retrieved excerpts only — it does not read the rest of the docs. - It answers from those excerpts even when they are brief, and only says it could not find that if they are about a different topic.
- The stream lists Relevant Guides (links to those pages) first, then Summary.
- The same browser IP is hashed and limited to 5 questions per hour and 10 per day (
consumeDocsChatRateLimit), stored indocs_chat_limits.
Chat History
docs_chat_session,docs_chat_message, anddocs_chat_limitstables in Supabase created byscripts/chat-history.sqlstore chat history per browser session and chat IP quotas.- The browser never talks to these tables — row-level security is on with no
anon/authenticatedpolicies./docs/chatreads and writes session and message rows with theservice_roleclient — quotas go throughDATABASE_URL.
docs_chat_sessionis one conversation — cookiedocs_chat_id(httpOnly, path/) stores that row's ID so opening ask the documentation again restores the same thread within that browser session.- Send upserts that session (a leftover cookie still captures) and writes the question immediately, then the reply after Gemini finishes.
- Clear Chat deletes that cookie and this session row.
docs_chat_messageis each turn in that conversation — the user's question or the assistant's reply (role), the wording (content), and for replies the retrieved page links (sources).- Deleting a session deletes associated messages.
docs_chat_limitsis the chat IP quota store — one row per hashed IP window (key), how many questions have been used (points), and when that window expires (expire).rate-limiter-flexiblewrites these rows.- Send hashes the browser IP and counts 5 questions per hour and 10 per day (
consumeDocsChatRateLimit). Next.js writes these rows throughDATABASE_URL, not theservice_roleclient. - Clear Chat does not reset quotas.
Chatbot Reference
Loading API reference…