Concepts

Seven ideas cover almost all of WitWiki: pages, links, sources, projects, the log, the index, and permissions.

Wiki pages

A page is the unit of knowledge. It has a path, a title, markdown content, and optional free-form metadata.

The path is the identity — concepts/auth, architecture/overview, decisions/why-postgres. Paths are lowercase and hyphen-separated, use / to express hierarchy, and never carry a .md extension. There is no separate "move" operation: writing to a new path creates a new page, so renaming means write-then- delete, and anything linking to the old path is left dangling until you fix it.

wiki_updatePage is an upsert and a full overwrite. It does not merge. The read-before-write loop is not a nicety — an agent that writes without reading first will silently destroy whatever was there.

Write [[target-path]] anywhere in page content, or [[target-path|display text]] when you want the prose to read differently from the path.

On every write the server re-parses the page and updates a directed graph of source → target edges. That graph is what powers /graph, the backlinks in the page gutter, and the orphan detection in wiki_lint.

Four rules decide what becomes an edge:

  • Links inside inline code or fenced code blocks are ignored, so you can document the syntax without polluting the graph
  • Duplicate targets are collapsed — first occurrence wins
  • A page cannot link to itself; self-references are dropped silently
  • Targets need not exist yet. A link to a page you haven't written is a "redlink" — it's tracked, and it shows up in wiki_lint as a dead link

That last rule is what makes the graph useful as a to-do list. Link to the page you wish existed, and lint will keep reminding you it doesn't.

Sources

Sources are the raw material: PDFs, meeting notes, exported transcripts, design docs. They are stored as blobs and are deliberately not wiki pages — the point is to distil them into pages, not to dump them in.

Each source moves through a status lifecycle:

uploaded → processing → processed → ingested
                     ↘ error

The step that matters is the last one. After an agent reads a source and folds its content into pages, it calls wiki_markSourceIngested. That flag is what stops the next session re-reading and re-summarising the same document forever. Nothing sets it automatically — an agent that never marks sources ingested will redo its own work indefinitely.

Projects and organisations

An organisation owns billing, members, and API keys. A project is a namespaced knowledge base inside it.

Every page, source, and log entry belongs to exactly one project. API keys are project-scoped, which is why agents never pass a project ID: the server reads it off the credential. One project per codebase or product area is the usual split.

If your wiki looks unexpectedly empty, the cause is almost always that you are reading a different project than the one you wrote to. See Troubleshooting.

The log

An append-only, chronological record of what happened. Entries have a kind (edit, ingest, query, or lint), a one-line summary, and optional structured details.

The log answers "what has my agent been doing?" in a way that page diffs cannot, because it captures intent rather than just outcome. Browse it at /log.

It is distinct from the audit log at /audit, which is a tamper-evident hash chain of security-relevant org events and is owner-only.

The index

A single page at the path index, and the conventional entry point to the wiki — a hand-curated table of contents rather than an automatic listing.

It matters more than it looks. An agent starting a session calls wiki_getIndex to orient itself; if the index is stale, the agent's picture of the knowledge base is stale too, and it will happily write a duplicate of a page it never found.

A brand-new project is seeded with a starter index, so wiki_getIndex returns something from the very first call. Replacing that starter page with a real overview of the project is the ideal first job for a freshly connected agent.

wiki_listPages({q: "..."}) runs PostgreSQL full-text search (websearch_to_tsquery) ranked by relevance. {prefix: "concepts/"} filters by path instead, and the two combine.

Search before you create. It is the single habit that keeps a wiki from fragmenting into six half-pages about the same subject.

Permissions

Two independent systems, and confusing them is the most common source of unexpected 403s.

Roles — who a member is

Roles form a strict hierarchy; each rank includes everything below it.

RoleRankAdds
owner50Billing, audit log, org settings, project transfer, promote/demote and remove anyone
admin40Invite members, set roles for and remove members below their own rank, delete pages and projects, manage connections
member30Write pages, append to the log, upload sources, create projects, rebuild the graph
viewer20Read everything, including the member list; create API keys for their own agents
agent10Read pages and sources — not the member list, and not the graph rebuild

Changing a role, and removing a member

Roles are editable in the members list in /settings, not fixed at invite time. Two rules govern who may change what, and removing a member follows the same rules — deleting someone is the same privilege as demoting them:

  • You can only hand out roles below your own, and only act on people below your own. An admin can move someone between member and viewer, and remove them; only an owner can create, demote, or remove an admin or another owner.
  • Nobody changes their own role, owner included, and nobody removes themselves. To step down, promote a second owner first and have them demote you — which is also the ownership handover flow, since an org can have more than one owner.

An org must always keep at least one owner: the last one can be neither demoted nor removed, because billing, the audit log, and org settings would become unreachable.

Two more details the table cannot show:

  • owner cannot be invited. Invites accept only admin, member, or viewer; ownership is granted by promoting an existing member.
  • agent is not a seat. It is the role an API key carries, so it never consumes a member slot, and it is never assignable to a person. It also ranks below viewer, which surprises people: an agent-role key cannot write pages over the REST API.

Scopes — what a key may do

Scopes apply to API keys only. Logged-in humans using the web app carry a session instead, and sessions are never scope-restricted.

ScopeGates
wiki:read / source:readReads (no scope is required for reads)
wiki:writewiki_updatePage, wiki_deletePage, wiki_updateIndex, wiki_appendLog, wiki_rebuildGraph
source:writesource_upload, wiki_deleteSource, wiki_markSourceIngested

An empty scope list means unrestricted, not read-only. Creating a key with no boxes ticked produces the most privileged key available, not the least. To restrict a key to reading, tick the read scopes explicitly.

Why the two systems disagree

The REST API enforces roles. MCP enforces scopes. The same credential can therefore succeed over one and fail over the other — most visibly with deletes, where MCP asks only for wiki:write but DELETE /api/pages/{path} demands admin (rank 40). An agent-role key deletes pages happily over MCP and gets a 403 over REST.

That is intended, not a bug: MCP is the agent surface and scopes are the agent control, while the REST API is the human surface and roles are the human control. Reach for the one matching the caller.