How an autonomous agent built a production wiki — and the six things it got wrong
tela is a self-hostable, markdown-native team wiki: a Go + Postgres backend, a React/Milkdown frontend, live Yjs collaboration, and a built-in MCP server so AI agents read and write pages alongside people. It's in production at telawiki.com. The first version — the schema, the API, the editor, the collab transport, the deploy stack — was written end to end by an autonomous coding agent. It's now under normal human development, and the git history is the source of truth for what the agent did and what we had to undo.
That last part is the interesting part. "An AI wrote an app" is not a finding anymore; everyone has seen the demo. The finding is where the seams are — which of the agent's decisions were quietly excellent and which ones a human had to rip out once the thing met real traffic. This is a build log of both, grounded in the actual code.
What it got right (and why it's surprising)
There is no block table. Markdown is the database. The single best architectural call is also the one most "modern wiki" products got wrong: pages.body is a TEXT column holding canonical markdown, forever. No blocks table, no JSON document tree, no Notion-style block model. The Milkdown editor reads and writes markdown; rich features (callouts, collapsibles, tabs, diagrams, [[wikilinks]]) are markdown extensions and ProseMirror decorations, not rows in a content tree. The payoff compounds: trivial import/export, full-text search straight over the column, line-diff history, WebDAV handing raw bytes to rclone, and an agent writing over MCP producing exactly what a human typing produces — same bytes, no serialization layer to disagree about. Every team that builds a block model spends two years writing format adapters. The agent just didn't.
Hand-written SQL, no ORM, no sqlc. database/sql on pgx/v5, positional $1 placeholders, RETURNING. For a schema this size that's the right amount of machinery — real SQL you can read, no codegen, no lazy-loading surprises. An agent reaching for an ORM "because that's what apps use" would have been the templated, wrong choice. It didn't.
Yjs is an overlay, not the truth. Live collab is Yjs + y-prosemirror, but the CRDT is not the source of truth — pages.body markdown is. Yjs rebases onto the canonical markdown on save, and its imports are confined to one directory plus a single editor branch. The transport is a custom 1-byte-tag WebSocket protocol, not off-the-shelf y-websocket. Collab is additive and removable — you can read, search, diff, export, and sync with the entire CRDT layer torn out. Most teams make the Y.Doc the system of record and then can never get clean markdown out again. The agent kept the escape hatch.
The MCP server is a first-class surface, not a bolt-on. Agents read and write through the same operation cores the REST API uses; row-returning tools wrap results in typed envelopes; writes are idempotency-keyed so a retried create_page doesn't double-post. Genuinely well-built — and also where the agent made its most instructive mistake.
The six things it got wrong
1. It chose a database to avoid running a database — then it got torn out
The original backend ran pure-Go SQLite (no cgo) with FTS5, to avoid operating a separate database — a very agent-shaped, demo-optimized call. Commit d8a4b6d ripped it out wholesale (every ?→$N, LastInsertId→RETURNING, INSERT OR IGNORE→ON CONFLICT; the SQLite migrations squashed to a Postgres baseline). The roadmap — concurrent writes, ranked full-text, and pgvector semantic search in one engine — needed a real database, and SQLite's :memory:-is-per-connection trap broke pooled tests. The agent optimized the opening move and under-weighted where the roadmap obviously went.
2. It built the MCP surface twice, and the copy drifted
The original design made the MCP server a standalone TypeScript package that re-implemented the tool surface, slug/URL building (which had to stay byte-parity with the Go side), and the error envelope agents key on. Two codebases that must agree forever is a drift generator, and it drifted. The v0.7 rewrite moved the server inside the Go backend — every tool now calls the same core function the REST route calls — and deleted the TypeScript implementation; the npm package survives as a thin stdio↔HTTP proxy with zero tool knowledge. The agent picked the architecture that looked clean as a diagram over the one that's cheap to maintain.
3. It made you boot a collaborative editor just to read
For most of tela's life, opening any page mounted the full collaborative Milkdown editor — dozens of ProseMirror plugins plus a Yjs snapshot fetch — even to read. Measured cost: roughly 440 ms of editor first-mount per session plus ~180 ms of body-blank on every page open. A human split reading from editing: reading now renders markdown straight to React, and the editor mounts only on an explicit Edit. The agent's instinct was to make the wrong architecture faster; the fix was to not load the editor at all.
4. It did not think adversarially
A long tail of security fixes a human backfilled: a private-source credential token that leaked onto a stored field (into a visible page and into logs); OG-card endpoints that served private-space content to any bot user-agent; javascript: hrefs that sailed through the renderer; an SSRF hole in the paste-a-link unfurl; no rate limiting on login or unfurl. None exotic — the standard "what does an attacker do with this endpoint" checklist the agent shipped without running. Every one is fixed; every one was a thing a human had to think to look for.
5. It over-built abstractions nobody asked for
It built an instance-wide feature-flag layer for features that are simply on whenever their dependency is reachable — deleted wholesale in commit 0683739. The MCP design speced the entire protocol surface (prompts, completions, elicitation, sampling, widgets); hosts don't surface most of it, so a whole phase was cut. Reaching for completeness where the right move was the three things hosts actually reward.
6. It left rough edges behind its own refactors
The Postgres migration left search as — per its own commit message — an unranked ILIKE placeholder until a human rebuilt ranked Postgres full-text. It also baked in permanent debt: keeping SQLite-era TEXT datetimes and INTEGER booleans, which strict pgx rejects, forcing CASE WHEN … THEN 1 ELSE 0 END shims across the codebase that nobody removed. Good at the big rewrite, bad at the unglamorous finishing pass.
What this actually says about agent-built software
The agent was strong where the problem is legible in the code: data modeling, API shape, keeping a CRDT contained, wiring a protocol. It was weak where the signal lives outside the code — the roadmap, the threat model, production traces, third-party host behavior, and the discipline to finish a migration rather than leave a TODO. Almost none of the human follow-up was "the agent wrote a bug." It was "the agent made a locally-reasonable decision without the context that would have made it globally-correct." It didn't build a bad wiki. It built a real one — and handed over an honest map of where judgment, not code, was the missing input.