§ Codex — the learning wiki · Architecture — one container, three faces
The data model
Two tables. That's the whole schema.
sections
A top-level area — one per project. Columns that matter: slug (unique, used in URLs), title, blurb (one-line description shown on cards), accent (one of six colour names that re-skins the section), and sort.
pages
Everything you read. Key columns:
section_id→ which section it belongs to (cascade-deletes with the section).parent_id→ self-referential foreign key to another row inpages. This is the entire nesting mechanism: a page withparent_id = NULLis a top-level page; a page that points at another page is a subpage. One level deep by convention.UNIQUE(section_id, slug)→ a slug only has to be unique within its section, so two projects can both have an "overview" page.
How the tree is built
The database stores a flat list of rows with parent pointers. The tree() function in db.js does the assembly in memory: load all pages for a section, build a Map from id → node, then walk the list and push each node into its parent's children array (or into roots if it has no parent). That's an adjacency-list → nested-object transform — cheap, and it avoids recursive SQL.
const byId = new Map(pages.map(p => [p.id, { ...p, children: [] }]));
const roots = [];
for (const p of pages)
(p.parent_id ? byId.get(p.parent_id).children : roots).push(byId.get(p.id));
Gotcha: because nesting is just a nullable
parent_id, nothing in the schema stops you nesting three levels deep. The one-level convention is enforced by discipline (and the MCP tool descriptions), not a constraint. Keep it shallow on purpose.