Build Tettra
KINDAback to the verdict
An internal knowledge base whose hard problems are making staleness visible everywhere a page appears rather than in a banner, turning questions into a queue that produces pages, and capturing chat questions without mirroring a chat history you have no right to hold.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the wiki.
Get it nodejs.org, or your version manager. open ↗
Verify
node --version prints v22 or higher - accountfree
Why The rate limits and the permissions for apps distributed outside a platform's marketplace are materially worse, and a wiki for your own team has no reason to be one.
Get it Create the app in your workspace's developer console, subscribe only to events addressed to it, and request the narrowest scopes that let it reply.
Verify
A message mentioning the app delivers an event and a reply posts back - installfree
Why This holds internal documentation for a team, so it is reachable over the network and should never be reachable over plain http.
Get it Any proxy you already run, with a certificate for the internal hostname.
Verify
Plain http is redirected and the certificate is trusted on a colleague's machine - have readyfree
Why Staleness, categories and the question queue all behave differently against real content than against a demo page, and the design decisions you make on fixtures will be wrong.
Get it Export what you have from wherever it currently lives, including the messy pages.
Verify
At least five of the pages have no obvious owner, which is the case the build has to handle - decidefree
Why One global interval produces either constant nagging or none at all, and choosing per category is the decision that makes the queue survivable.
Get it Write down an interval for each category, shortest for anything describing a process that changes.
Verify
Intervals are configured per category and the queue is a manageable size after seeding
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- pages(id, slug, title, body_md, category_id, owner_id, created_at, updated_at, verify_every_days, verified_at, verified_by, archived_at) - revisions(id, page_id, ordinal, body_md, title, author_id, created_at, summary) · every save, never pruned - categories(id, name, parent_id, ordinal) - questions(id, asked_by, body, source, source_ref, asked_at, state, answer_page_id, answered_at, answered_by) · source is web or chat and state is open, answered or declined - page_questions(page_id, question_id) · the questions a page was written to answer stay attached to it - verifications(id, page_id, user_id, at, outcome, note) · outcome is confirmed, edited or retired - users(id, name, email, role, active) · role is reader, author or admin - A page is stale when verified_at plus verify_every_days is in the past. That expression appears in the search query and the link renderer, not only in a banner on the page itself.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/wiki.db | Pages, revisions, questions and verifications. |
BACKUP_DIR | required | ./data/backups | Automatic backups. The restore path is exercised in the suite. |
DEFAULT_VERIFY_DAYS | required | 180 | Fallback interval when a category does not set one. |
CHAT_SIGNING_SECRETsecret | optional | redacted | Verifies incoming chat event signatures. Reject anything unsigned. |
CHAT_BOT_TOKENsecret | optional | xoxb-...redacted | Posts replies only. No history scopes should be attached to it. |
EXPORT_DIR | required | ./exports | Markdown files with front matter, plus the revision archive. |
The build, in order
Pages with an owner and a revision trail
Content is Markdown with every save kept, and no page exists without a named owner.
Enforced at insert, not requested in the interface.
Files
src/db/schema.sqlsrc/pages/create.tsAppend only, with author and summary.
Files
src/pages/save.tstests/revisions.test.tsThis is the moment ownership actually gets decided.
Files
src/users/deactivate.ts
done when · tick each as it passeswatch out- Letting ownership be optional means every page written in the first fortnight has no owner, and by the time anyone wants to know who is responsible for the onboarding page, the person who wrote it has left and the field is still empty. Require an owner on insert, and when a user is deactivated force reassignment of their pages before the deactivation completes.
Staleness that follows the page everywhere
An expired page is visibly stale in search results, in links to it, and in the owner's queue.
So every surface inherits it without being told.
Files
src/pages/queries.tstests/staleness.test.tsThe reader sees it before they click, which is the only useful moment.
Files
src/render/links.tsThree actions, each recorded, none of them a dismiss button.
Files
src/app/queue/page.tsxsrc/pages/verify.ts
done when · tick each as it passeswatch out- Putting a yellow bar at the top of an out-of-date page changes nothing, because the reader who arrived from search has already read the wrong paragraph before scrolling up, and the reader who arrived from a link never sees it. Carry the state into the search ranking and into every rendered internal link, so a stale page looks stale before it is opened.
Questions are rows until they are pages
An unanswered question is tracked, assigned and closed by producing or linking a page.
A free-text answer is knowledge that evaporates.
Files
src/questions/state.tstests/questions.test.tsThe right path has to be the fastest one.
Files
src/app/pages/new/page.tsxSo a page can be judged against what it was for.
Files
src/db/schema.sql
done when · tick each as it passeswatch out- Letting a question be closed with a free-text answer produces a helpful reply that lives in the question and nowhere else, which means the next person asks the same thing and the wiki never grows. Make page or decline the only two exits, and let the page creation form be prefilled from the question so the easy path is also the right one.
Questions from chat, without mirroring chat
A question asked in team chat becomes a row through an event subscription, and no chat history is ever read.
Which is also the only thing the platform will reliably let you have.
Files
src/chat/events.tstests/no-history-calls.test.tsEvery event platform redelivers, and a duplicate question is noise.
Files
src/chat/dedupe.tsSo the asker sees the answer where they asked.
Files
src/chat/reply.ts
done when · tick each as it passeswatch out- The obvious design is to read the channel history and mine it for questions, and the platform has closed that path: since 29 May 2025 Slack limits conversations.history and conversations.replies to one request per minute returning at most fifteen objects for newly created apps distributed outside its Marketplace, while apps built for a single workspace as internal custom apps keep the higher limits. Check the current published limits for whichever platform you use before you design around them, build this as an internal app for your own workspace, and take questions from events addressed to you rather than from history.
Reading rights, files out, and a wiki that survives youproduct builder
Access is per category, the whole wiki exports as Markdown files, and a restore is tested rather than assumed.
One place, not five, because five means four and a leak.
Files
src/auth/scope.tstests/restricted-surfaces.test.tsThe format check is whether a text editor can read it.
Files
src/export/wiki.tstests/roundtrip.test.tsA backup nobody has restored is a hope.
Files
src/backup/restore.tstests/restore.test.ts
done when · tick each as it passeswatch out- Filtering restricted pages in the template means the search index, the sitemap, the link renderer and the export each need the same filter added by hand, and one of them will be forgotten, usually the search snippet that shows the first two lines of a restricted page. Apply the restriction in the query layer that every surface goes through, and add a test that calls each surface as a restricted user.
That is the whole plan for Tettra. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Real-time collaborative editing.
- A chat history importer.
- Single sign-on and directory synchronisation.
- An answer-generating assistant over your pages.
- real-time multiplayer
- enterprise permissions
- large integration catalog
- mobile capture and notifications
- A monthly digest per owner instead of a queue page, once the queue is big enough that nobody opens it
- Page templates per category, which is the cheapest way to make new pages consistent
- A read-tracking signal on stale pages, so the ones people actually open get verified first
Need the files? The project pack on the verdict page hands your agent the whole brief · more tasks.