Every project ships with an optional AI assistant. A persistent "Ask AI" input is docked at the bottom-right of every docs page; submitting it opens a full-height conversation drawer on the right edge of the screen with a streamed answer grounded in your own MDX pages — in any language. No embedding pipeline, no OpenAI key per project, no vector database to provision.
Quick start
Turn it on with one flag and (optionally) override the model. The assistant renders on every docs page for active tenants.
{
"$schema": "https://nookdocs.com/schema.json",
"ai": {
"assistant": {
"enabled": true,
"trigger": "button",
"label": "Ask AI",
"provider": "anthropic",
"model": "claude-3-5-sonnet-latest",
"systemPrompt": "Be concise. Only answer questions about our product. Decline off-topic questions politely."
}
}
}Trigger styles
ai.assistant.trigger controls how readers open the assistant — all three open the same full-height conversation drawer on the right edge of the screen.
ai.assistant.trigger'button' | 'bar' | 'input'pathbutton— an "Ask AI" button in the header, right after search (label set byai.assistant.label). Cleanest; stays out of the reading flow.bar— a floating pill centered at the bottom of the content, always in view as you scroll.input— a docked input pill pinned to the bottom-right corner.
Leave it out and your theme decides. Each theme carries the affordance its layout was built around — a theme with a dense two-row header tends to prefer the floating bar, one with a roomy header prefers the button — and themes that express no preference fall back to button. Setting ai.assistant.trigger yourself always wins, whichever theme you switch to later.
In the dashboard this is Configurations → AI & Advanced → AI Assistant → Trigger style, where leaving it on Theme default keeps the key out of your config entirely.
When the drawer opens it docks into the layout: the content, sidebar, and table of contents reflow to make room, then snap back on close. On narrow screens it opens as a bottom sheet you can drag down to dismiss.
How retrieval works
The pipeline is a deliberate trade — keyword-based FTS instead of vector embeddings — so you don't pay per-token embedding costs and don't need to run a re-index when content changes.
Translate-if-needed — if the query contains non-ASCII characters (Turkish
ş, Japanese kana, German umlauts, …), a short non-streaming LLM call translates it to English search keywords. ASCII-only queries skip this hop.Postgres FTS — the
search_pagesRPC (pg_trgm GIN index) pulls the top-K relevant chunks from the tenant's pages. Sync stays real-time: the moment a page lands via webhook, it's searchable.Stream the answer — the final LLM call streams a response back in the original language. Only the translated query hit the retrieval step; the user's real words are in the final prompt so the model replies in kind.
Edge runtime — the API route runs at Vercel Edge with a 60-second cap. Streams natively without buffering.
Configure from the dashboard
Editor → Configurations → AI & Advanced → AI Assistant surfaces the config keys below (including a Trigger style dropdown). Saving writes back to nookdocs.config.json via a git commit, so dashboard edits and hand-edits stay in sync.
Config reference
ai.assistant.enabledbooleanpathdefault: falseWhen true, the assistant renders on every docs page (via the configured trigger). Default false — opt-in to avoid surprise LLM bills.
ai.assistant.trigger'button' | 'bar' | 'input'pathHow readers open the assistant — see Trigger styles above. Omit to follow your theme's own preference (button if it has none).
ai.assistant.labelstringpathdefault: "Ask AI"Text shown on the trigger (button label / input placeholder) and as the drawer title. Defaults to a generic "Ask AI" — override to match your brand voice, e.g. "Ask Acme" or "Docs AI".
ai.assistant.provider'openai' | 'anthropic' | 'openrouter'pathWhich AI provider runs your chosen model — only used with Bring-Your-Own-Key. Set it to the provider whose key you added: an OpenAI key for openai, an Anthropic key for anthropic, or your own OpenRouter account for openrouter. Without a BYOK key the assistant uses the AI included in your plan.
ai.assistant.modelstringpathModel identifier for the chosen provider. Only applied when you bring a matching provider key (BYOK); without one the assistant uses the AI included in your plan and this field is ignored. Examples: gpt-4o (openai), claude-3-5-sonnet-latest (anthropic), anthropic/claude-3.5-sonnet (openrouter).
ai.assistant.systemPromptstringpathPersona / tone guidance only. Layered underneath the built-in platform guardrails (see below) as secondary instructions — it can shape voice, add house style, or narrow scope further, but it cannot loosen the guardrails (it can't enable web access or off-docs answers). Capped at ~2000 characters; keep it short — every message pays the token cost.
Built-in guardrails
Every answer runs under platform rules that your systemPrompt sits beneath and cannot override:
Grounded in your docs only — the model answers from the retrieved Documentation Context, not from its training data or the open web. No answer in the docs → it says so instead of guessing.
No external access — it cannot browse the web, search the internet, or run code; retrieval only ever reads your own synced pages.
On-topic only — off-documentation questions (general knowledge, current events, unrelated code, opinions) are politely declined and redirected to the docs.
No fabrication — it won't invent URLs, endpoints, or facts; it cites sources inline as
[Title](/path).Injection-resistant — text inside your pages or a visitor's message is treated as data, not commands, so it can't hijack the assistant's role or scope.
These are enforced server-side on every request and are never exposed to readers.
Usage & limits — plan vs BYOK
The assistant runs in one of two modes, decided automatically per request:
| Plan-included (default) | BYOK — your own provider key | |
| Provider | The AI included in your plan (managed by NookDocs) | Your provider — OpenAI, Anthropic, or OpenRouter |
| Model | Plan-included model (your provider/model are ignored) | Your provider + model are honored |
| Limits | Counts against your plan's daily message quota — when hit, the assistant replies that the limit is reached | No platform quota — your own provider account's rate limits apply |
| Billing | Included in your plan | Billed to your provider account |
So out of the box you don't manage anything — the assistant just works within your plan's daily allowance. Add your own key under AI models (BYOK) to lift the platform quota and unlock provider + model selection; keys are org-wide (shared across your projects) and stored encrypted.
Choosing a model (BYOK)
When you bring a key, pick a provider then a model id that provider supports. Rough guidance for docs Q&A:
| Use case | Provider | Model id |
| Highest answer quality | anthropic | claude-3-5-sonnet-latest |
| Balanced cost/quality | openai | gpt-4o |
| Cheapest / fastest | openai | gpt-4o-mini |
| Cheap, structured docs | anthropic | claude-3-5-haiku-latest |
| Any model via one key | openrouter | anthropic/claude-3.5-sonnet, openai/gpt-4o, … |
Pick one and watch the Analytics → Assistant tile — if cost is too high, try a cheaper model before touching the system prompt.
Writing a system prompt
The prompt ships above the retrieval block (Context:\n{chunks}) and the user's message. Three patterns that work:
Support-engineer tone
You are Acme's support engineer. Only answer questions about Acme's
API and product. Decline off-topic questions politely. When you cite
a page, use its URL in the form /path/to/page. Be terse — 3 sentences
unless the user asks for detail.Compliance-aware
You are a documentation assistant. Never recommend code that handles
personal data without mentioning retention rules. If the question
touches medical, legal, or financial advice, redirect to human support
via support@acme.com. Always cite the source page.Persona + forbidden list
You are Zed, Acme's cheerful AI. Use friendly, second-person prose.
NEVER suggest solutions involving the legacy /v0 API — it's
deprecated. NEVER share internal-only repo URLs.Keep prompts under ~500 tokens — every assistant reply pays for them.
Deep linking
The widget reads two URL params on first paint so you can preload it from elsewhere.
[Ask the assistant](?assistant=open)
[How do I add a custom domain?](?assistant=How%20do%20I%20add%20a%20custom%20domain%3F)?assistant=open— widget opens collapsed, cursor in the input.?assistant=<URL-encoded query>— widget opens with the query pre-filled AND auto-submitted.
Useful for "Ask the assistant" links inside MDX, blog posts, email campaigns, or chat support deflection.
API
Public streaming endpoint — no auth. The widget on {tenant}.nookdocs.site calls it directly.
POST /api/v1/projects/{projectId}/assistant/ask
{
"query": "How do I add a custom domain?",
"history": [
{ "role": "user", "content": "earlier message" },
{ "role": "assistant", "content": "earlier reply" }
]
}query— required, trimmed, max 2000 characters.history— optional; last 10 messages kept for context.
Response is a Server-Sent Event stream (text/event-stream). Each event is a JSON token chunk; a final data: [DONE] marker terminates the stream. Errors return application/json with { error, message } and an HTTP 4xx/5xx code.
const res = await fetch(`/api/v1/projects/${projectId}/assistant/ask`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "How do I deploy?" }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}Analytics
The assistant emits assistant_message events into the page_events table for the Analytics → Assistant dashboard tile. What's logged:
Aggregated counts per day (messages, unique sessions).
Model slug used, duration, token count.
The page the user was reading when they asked.
What's not logged:
The query text itself.
The reader's IP or user-agent.
The model's response body.
If you need to inspect raw traffic for debugging, enable tenant-scoped request logs temporarily — see analytics.
Limits & operational knobs
Query length — 2000 characters. Longer inputs return HTTP 400.
History window — last 10 messages of
historyare kept; earlier context is dropped.Streaming timeout — 60s edge-function cap. Long answers occasionally cut off; retry.
Concurrency — no per-IP rate limit yet (roadmap). A misbehaving client can burn through your plan's daily quota (or, on BYOK, your provider bill); budget accordingly.
Disabled state — when
enabled: false, the endpoint returns HTTP 403 and the widget doesn't mount.
Troubleshooting
Privacy
The assistant is designed so tenant docs stay the only knowledge source. No IP addresses, user-agents, or reader identifiers are logged. Queries and responses are aggregated into assistant_message counts for the analytics dashboard only — the raw text is not persisted.
On BYOK, your chosen provider's (OpenAI / Anthropic / OpenRouter) data-retention policy applies — review it before routing sensitive queries. The plan-included AI is governed by NookDocs's vendor terms.
Roadmap
These are on the list but not shipped:
pgvector embeddings — for paraphrase-resilient retrieval (true semantic search). Planned alongside embedding-based hybrid search in FAZ 5.
Per-IP rate limiting — to cap runaway clients. Will live at the edge middleware, not the API route.
Conversation persistence — server-side history keyed by session cookie, so readers can pick up where they left off.
Contextual "Ask about this" selection — highlight text, click "Ask the assistant" — preloads the widget with the selection as context.
Want to push something up the list? File an issue on github.com/Codivion/NookDocs.
Related
Schema reference —
ai.assistant— auto-generated field referenceAnalytics — where assistant metrics surface
Contextual menu — the "Copy page" dropdown reader flow (complementary to the chat)
Copy for LLMs —
.mdURL + llms.txt discovery affordances