Knowledge base
Collections from outside sources, imports and connectors, runbooks with stable step ids, the public site, grants, and the /kb API.
A knowledge base article is never a document and never belongs to a company. Collections hold reference articles from outside sources: a vendor’s help center read by its own structure, a website crawled by sitemap or from a starting page, a wiki or another system’s export brought in as a zip or a folder, with their pictures and the PDFs and Word documents they link.
Bringing documentation in
Imports upsert on (collection, source key), so a newer export updates what changed and nothing else. Every converter and every stored body is held to one standard: an imported article must read as its source did.
- Numbered steps keep their numbers and keep counting. A picture, a note, or a caption between two steps belongs to the step above it. A list restarts only where the source restarts.
- A source site’s own navigation is never kept: breadcrumbs, “Contents” lists of anchor links, print and share buttons. Trove KB builds its own outline from the headings.
- A section title the source numbers is a heading, not an empty list item.
images/andfiles/folders in an archive are attachments; a crawled page’s pictures are kept by their address; the original document is kept and offered.- Categories come from the source’s structure, and a folder named on import overrides them.
Check a new source by importing it and reading three articles with steps against their originals before importing the rest.
Admin → Knowledge base drives imports from the browser in pieces, resumable. An archive already on the server can be imported without the browser:
npx tsx --conditions react-server scripts/kb-import.ts "<collection>" <file.zip>
Run it where STORAGE_PATH is the deployment’s own uploads volume: the pictures in the archive are written there.
Runbooks
A runbook is an article whose lists are a procedure. Its steps are read from the body on every save: the items of every top-level list that is a procedure, in order across headings, each as { id, text, note?, canned? }.
The id is stable: write it yourself at the end of the item as {#my-id} ([a-z0-9-]{1,40}), or let Trove KB mint one on first save, after which it is in the stored body and kept across edits. A system that tracks progress through a runbook keys its state by id and keeps that state on its own side; Trove KB stores none. note is whatever was nested under the item; canned is the name in the first @canned:[Name] token of the step. Two steps with one id are refused.
The public site
/pub/kb is for readers who have not signed in. It belongs on a hostname of its own, such as kb.yourdomain.com, behind a proxy that passes /pub/kb, /_next/static/, the logo, and the favicon, and refuses everything else. See Cloudflare for the nginx block.
- Each collection has Show on the public site, off by default. An article can be held back from its own page.
- Keyword rules (phrases or regular expressions, by title, category, or file type) and per-category switches keep the rest off, and apply to what arrives later.
- Under Admin → Settings → Public knowledge base, choose who is admitted: anyone who can reach it, or only visitors from listed addresses. Pair it with a Cloudflare Access Bypass policy for the same ranges.
- Behind Access, name the team and the application’s audience tag and readers keep favorites and votes: Trove KB checks the token Access adds to each request against the team’s published keys and knows the reader by a hash, with no account of its own.
Grants
A person granted a collection reads it, and with can_write writes to it in the app, whatever companies it is kept to; administrators need no grant. An account can be made ahead of a person’s first sign-in so the grant is waiting for them. Under Admin → Knowledge base → API access, an administrator sets one key’s access to every collection at once: D (only what its companies allow), R, RW, and whether the key may keep reactions there.
API
GET /kb/collections?writable=true
GET /kb/collections/:id?kind=
GET /kb/search?q=&collection_id=&category=&kind=&limit=&cursor=
GET /kb/articles?collection_id=&category=&subcategory=&kind=&updated_since=&sort=&dir=&limit=&cursor=
GET /kb/articles/:id full body, kind, steps, external_id, source_url, public_url
PUT /kb/collections/:id/articles/:external_id upsert (write scope + write grant)
DELETE /kb/collections/:id/articles/:external_id archive (write scope + write grant)
PUT /kb/collections/:id/grants/users/:userId { can_write? }
DELETE /kb/collections/:id/grants/users/:userId
PUT /kb/collections/:id/grants/api-keys/:keyId { can_write?, reactions? }
DELETE /kb/collections/:id/grants/api-keys/:keyId
Every article item carries kind, source_type (md, html, pdf, docx, txt), public, favorites, and helpfulness. audience=public narrows a read to what the public site shows. A PUT answers 201 when it created the article and 200 when it replaced it.
Favorites and votes for a named reader
With the reactions scope and the header X-Trove-Reader: <email> (never stored; a key is derived from it, the same one the public site uses behind Access):
GET /kb/articles/:id/reactions -> { favorites, helpful_up, helpful_down, helpfulness, mine }
PUT /kb/articles/:id/favorite -> 204 DELETE -> 204
PUT /kb/articles/:id/vote { helpful: true|false } -> 204 DELETE -> 204
GET /kb/favorites?limit=&cursor=
Webhooks kb.article.upserted and kb.article.archived are sent for writes through the API, MCP, or the in-app editor.