Build Tettra

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNext.js 15 with TypeScript, one process, small team behind a reverse proxyStorageSQLite with every page revision keptContentMarkdown stored as text, rendered server side, no proprietary block formatStalenessA verification interval per page, enforced in queries not in a bannerChat captureEvent subscriptions for messages addressed to the app, never history polling

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the wiki.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. free

    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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/wiki.dbPages, revisions, questions and verifications.
BACKUP_DIRrequired./data/backupsAutomatic backups. The restore path is exercised in the suite.
DEFAULT_VERIFY_DAYSrequired180Fallback interval when a category does not set one.
CHAT_SIGNING_SECRETsecretoptionalredactedVerifies incoming chat event signatures. Reject anything unsigned.
CHAT_BOT_TOKENsecretoptionalxoxb-...redactedPosts replies only. No history scopes should be attached to it.
EXPORT_DIRrequired./exportsMarkdown files with front matter, plus the revision archive.

The build, in order

  1. Pages with an owner and a revision trail

    Content is Markdown with every save kept, and no page exists without a named owner.

    1. Enforced at insert, not requested in the interface.

      Files src/db/schema.sqlsrc/pages/create.ts

    2. Append only, with author and summary.

      Files src/pages/save.tstests/revisions.test.ts

    3. This is the moment ownership actually gets decided.

      Files src/users/deactivate.ts

    done when · tick each as it passes
    watch 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.
  2. 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.

    1. So every surface inherits it without being told.

      Files src/pages/queries.tstests/staleness.test.ts

    2. The reader sees it before they click, which is the only useful moment.

      Files src/render/links.ts

    3. Three actions, each recorded, none of them a dismiss button.

      Files src/app/queue/page.tsxsrc/pages/verify.ts

    done when · tick each as it passes
    watch 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.
  3. Questions are rows until they are pages

    An unanswered question is tracked, assigned and closed by producing or linking a page.

    1. A free-text answer is knowledge that evaporates.

      Files src/questions/state.tstests/questions.test.ts

    2. The right path has to be the fastest one.

      Files src/app/pages/new/page.tsx

    3. So a page can be judged against what it was for.

      Files src/db/schema.sql

    done when · tick each as it passes
    watch 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.
  4. 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.

    1. Which is also the only thing the platform will reliably let you have.

      Files src/chat/events.tstests/no-history-calls.test.ts

    2. Every event platform redelivers, and a duplicate question is noise.

      Files src/chat/dedupe.ts

    3. So the asker sees the answer where they asked.

      Files src/chat/reply.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more tasks.