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.
Wikilinks and the link graph
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 codeor 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_lintas 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.
Search
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.
| Role | Rank | Adds |
|---|---|---|
owner | 50 | Billing, audit log, org settings, project transfer, promote/demote and remove anyone |
admin | 40 | Invite members, set roles for and remove members below their own rank, delete pages and projects, manage connections |
member | 30 | Write pages, append to the log, upload sources, create projects, rebuild the graph |
viewer | 20 | Read everything, including the member list; create API keys for their own agents |
agent | 10 | Read 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
memberandviewer, 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:
ownercannot be invited. Invites accept onlyadmin,member, orviewer; ownership is granted by promoting an existing member.agentis 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.
| Scope | Gates |
|---|---|
wiki:read / source:read | Reads (no scope is required for reads) |
wiki:write | wiki_updatePage, wiki_deletePage, wiki_updateIndex, wiki_appendLog, wiki_rebuildGraph |
source:write | source_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.