Skip to content

PRD: infinite-docs — infinitely-nestable architecture & documentation tool #2

@CuriouslyCory

Description

@CuriouslyCory

Problem Statement

Engineers and teams have no good way to document software architecture so that it stays current, stays navigable, and is useful to both humans and AI agents at the same time.

  • Static diagramming tools (Visio, Lucid, draw.io) produce pictures that rot the moment the system changes, can't be drilled into, and carry no real documentation.
  • Text docs and wikis capture detail but lose the spatial, relational picture — you can't see how infrastructure, services, and data connect.
  • Neither form feeds cleanly into an LLM, and none let an AI agent read and maintain the architecture as it works on the actual system.

What's missing is a single place where you can model a system at any depth — from infrastructure down to individual database tables — keep it alive as the system evolves, and hand it to both people and agents as a first-class artifact.

Solution

A drag-and-drop Canvas where you place Components and draw Connections between them. Clicking a Component performs a Descent into its own interior Canvas — recursing to arbitrary depth (e.g. infrastructure → a host → its services → a module → a table). The external systems a Component connects to on its parent Canvas appear inside it as read-only boundary proxies, inherited transitively down the subtree, so dependency context follows you all the way down.

Every Component carries markdown documentation. The entire graph — or any subtree — exports to deterministic markdown so it can be pasted into an LLM with stable, diffable results. Capability-URL sharing lets teammates view a project read-only, and an authenticated MCP server lets AI agents read the architecture and build/maintain it (create Components, connect them, write docs) as they work on the real system. All destructive edits are recoverable via soft-delete + undo, which matters precisely because agents will be making changes.

This PRD covers the full product vision, sequenced across milestones M0–M5. The work is structured so the core slice (data model + service layer + first nested Canvas) is buildable and verifiable on its own before later milestones layer on markdown export, boundary propagation, and the MCP server.

User Stories

Authentication & projects

  1. As an engineer, I want to sign in with Discord, so that my architecture projects are private to me by default.
  2. As an engineer, I want to create a new project, so that I have a root Canvas on which to model a system.
  3. As an engineer, I want to see a list of my projects, so that I can return to ongoing work.
  4. As an engineer, I want to rename a project, so that its title reflects the system it documents.
  5. As an engineer, I want every project to have an unguessable capability URL, so that sharing one project never exposes my others.

Canvas, Components & Connections

  1. As an engineer, I want to add a Component to the Canvas, so that I can represent a part of my system.
  2. As an engineer, I want to set a Component's kind (service, database, external API, host, queue, or generic), so that it is visually distinguishable by icon and color.
  3. As an engineer, I want to reposition a Component by dragging, so that the layout matches how I think about the system.
  4. As an engineer, I want Component drags to feel instant and persist automatically, so that I never think about saving.
  5. As an engineer, I want to rename a Component inline, so that its label stays accurate.
  6. As an engineer, I want to draw a Connection between two Components, so that I can show how they relate or how data flows.
  7. As an engineer, I want to label a Connection and set its direction, so that the relationship's meaning and flow are explicit.
  8. As an engineer, I want Connections only to be drawable between Components that live on the same Canvas (or to a boundary proxy), so that the model stays coherent.
  9. As an engineer, I want newly created Components and Connections to appear immediately (optimistically), so that the tool feels fast even on a slow connection.

Descent, nesting & boundary

  1. As an engineer, I want to click into a Component to descend into its interior Canvas, so that I can document its internals at any depth.
  2. As an engineer, I want a breadcrumb trail of my path from the root, so that I always know where I am and can navigate back up.
  3. As an engineer, I want Descent to feel instant (the child Canvas prefetched), so that drilling into the architecture never blocks me.
  4. As an engineer, I want the external systems a Component connects to on its parent Canvas to appear as boundary proxies inside it, so that I can reason about its dependencies while documenting its internals.
  5. As an engineer, I want boundary proxies inherited transitively down the whole subtree, so that a deeply nested Component still shows the external systems its ancestors depend on.
  6. As an engineer, I want boundary proxies to be visually distinct and read-only, so that I never confuse them with Components I can descend into.
  7. As an engineer, I want inherited boundary proxies to be collapsible/grouped, so that deep Canvases stay uncluttered.
  8. As an engineer, I want to draw a refinement Connection from an interior Component to a boundary proxy, so that I can document exactly how a Component fulfills its parent's external dependency. (v2 / M5)

Documentation & markdown export

  1. As an engineer, I want to write markdown documentation on each Component, so that the diagram carries real explanatory content.
  2. As an engineer, I want doc edits to autosave (debounced, optimistic), so that I never lose work or manage a save button.
  3. As an engineer, I want a Component's markdown rendered nicely in-app, so that documentation is pleasant to read.
  4. As an engineer, I want to export a project or any subtree as deterministic markdown, so that I can paste it into an LLM and get stable, diffable output.
  5. As an engineer, I want the export to include Connections and boundary context, so that the exported document fully describes the architecture in isolation.
  6. As an engineer, I want a "Copy as markdown" action on any Component, so that I can quickly feed a subtree to an AI assistant.

Soft-delete & undo

  1. As an engineer, I want to delete a Component, so that I can remove parts of the model that no longer apply.
  2. As an engineer, I want deleting a Component to also remove its descendants and incident Connections, so that the model never leaves orphans.
  3. As an engineer, I want to undo a deletion, so that I can recover an accidentally removed subtree.
  4. As an engineer, I want deletions to be recoverable even when made by an AI agent, so that automated maintenance can never permanently destroy my work.

Sharing & viewing

  1. As a project owner, I want anyone with the capability URL to view my project read-only, so that I can share architecture without granting edit access.
  2. As a viewer, I want to open a shared capability URL and navigate the Canvas and descend into Components, so that I can explore the architecture freely.
  3. As a viewer, I want to be unable to edit a project I only have a view link to, so that the owner's model stays protected.
  4. As an engineer, I want editing to require being the signed-in owner, so that a leaked view link can never be used to tamper.

MCP & AI agents

  1. As an engineer, I want to mint and revoke API tokens for agents from a "Connect an agent" page, so that I control which agents can act and can cut them off instantly.
  2. As an engineer, I want each token shown only once and stored hashed, so that token leakage risk is minimized.
  3. As an AI agent, I want to authenticate to the MCP server with a bearer token, so that I act on behalf of exactly one user securely.
  4. As an AI agent, I want my token scoped only to its owner's projects, so that I can never read or modify another user's data.
  5. As an AI agent, I want to read a project or subtree as markdown via MCP resources, so that I can understand the system I'm working on.
  6. As an AI agent, I want a cheap project index/table-of-contents resource, so that I can map a large architecture before drilling in without burning tokens.
  7. As an AI agent, I want tools to create Components, create child Components, connect Components, update documentation, and move Components, so that I can build and maintain the architecture as I work on the system.
  8. As an AI agent, I want a single transactional "apply graph" tool, so that I can construct an entire architecture from a description in one call instead of dozens.
  9. As an AI agent, I want tool errors returned as readable messages rather than crashes, so that I can self-correct.
  10. As an engineer, I want the MCP endpoint to reject every unauthenticated request, so that my architecture is never exposed anonymously.
  11. As a developer wiring up an agent, I want a discovery file (llms.txt) and a copy-paste client config, so that I can connect Claude Code / Cursor in minutes.

Performance (cross-cutting)

  1. As an engineer, I want any Canvas to load in a single round trip, so that opening or descending into a project has no request waterfall.
  2. As an engineer, I want the Canvas to stay at 60fps while dragging, so that editing feels native rather than networked.

Implementation Decisions

Terminology (canonical → layer). "Node" is overloaded (Node.js, the canvas library). The user-facing / docs / MCP-verb term is Component; the data-model + graph-code term is Node. Likewise Connection (user-facing) vs Edge (data). A Canvas is a derived view, not a stored entity: { Nodes with parentId = N } + { Edges with canvasNodeId = N }. Descent = entering a Component's interior Canvas.

Domain model. Three soft-deletable content models plus an agent-token model. The field set below encodes the data-model decision (the keystone being that an Edge's scope — the Canvas it is painted on — is explicit via canvasNodeId, never derived, because refinement Edges legitimately span scope levels):

enum NodeKind { GENERIC SERVICE DATABASE EXTERNAL_API HOST QUEUE }   // cosmetic: drives icon/color
enum EdgeDirection { NONE FORWARD BIDIRECTIONAL }

Project { id, title, ownerId, slug @unique (capability URL), createdAt, updatedAt, deletedAt? }
Node {
  id, projectId, parentId? (null => root Canvas),
  title, kind: NodeKind, posX, posY, documentation, metadata: Json?,
  createdAt, updatedAt, deletedAt?
  // indexes: (projectId, parentId), (parentId)
}
Edge {
  id, projectId, canvasNodeId? (the Canvas it is painted on; null => root),
  sourceId, targetId, label?, direction: EdgeDirection, kind?, metadata: Json?,
  createdAt, updatedAt, deletedAt?
  // indexes: (projectId, canvasNodeId), (sourceId), (targetId)
}
ApiToken { id, userId, name, hashedToken @unique, prefix, scopes[], lastUsedAt?, expiresAt, revokedAt?, createdAt }   // M4

Service layer is the single deep module and the only home for business logic + authorization. Every operation is a pure-ish function with the signature (db, actor, input) => result, where actor = { userId, scopes?, via?: "session" | "token" }. Both the tRPC procedures and the MCP tools are thin adapters that resolve an actor and call these functions — so authorization cannot be bypassed by the MCP path (the framework's auth guard does not protect MCP; access checks do). The module interface (the deliberately small, stable surface):

  • node operations: create, createChild, update (title/kind/docs), updatePositions (batch), move (reparent with cycle prevention), softDelete (cascades to descendants + incident edges, returns the affected id set as an undo token), restore
  • edge operations: connect (validates legal shape + same-Canvas + de-dupes active edges), update, softDelete, restore
  • canvas read: getCanvas → { interiorNodes, interiorEdges, boundaryProxies, breadcrumbs } in one round trip; breadcrumbs and boundary derived via a single recursive SQL CTE, never a per-level query walk
  • access: assertCanRead (owner OR valid capability-slug context) and assertCanWrite (owner only)
  • boundary: transitive derivation boundary(H) = directBoundary(H) ∪ boundary(H.parent); boundary proxies are derived, never persisted
  • serialize: deterministic markdown for a project / subtree / index
  • tokens (M4): mint, verify (→ actor), revoke

Invariants enforced in the service, not the database: no self-edge, no parentId cycle, legal Edge shapes (both endpoints on Canvas C, or one interior + one boundary of C), and no duplicate active Edge. (A Postgres partial-unique-index WHERE deletedAt IS NULL is a possible later hardening but is out of scope; service-level de-dupe is the decision for now.)

Soft-delete + undo. Deletes set deletedAt; all reads filter it out. Deleting a Component soft-deletes its whole subtree and incident/Canvas Edges in one operation and returns the affected set so the action is undoable. Foreign-key cascade is retained only as a hard-delete backstop (true project deletion / future GC).

Sharing via capability URL. The unguessable slug is the read capability: reads resolve a project by slug and are served to anyone (public read path), while every mutation requires the signed-in owner. The MCP server is always authenticated regardless — there is no anonymous agent access.

Optimistic, waterfall-free Canvas (perf model). The canvas library's internal node/edge state is the source of truth during interaction; the query cache is the persistence mirror, committed on drag-stop — never per frame. A drag updates in memory at 60fps and fires exactly one position mutation when it ends (the final position is already on screen). New Components/Connections render optimistically with a temporary id that is reconciled to the real id on success; failures roll back with a toast. The interior Canvas is prefetched on Component hover so Descent feels instant. The Canvas subtree is force-remounted per Canvas (keyed by the current canvasNodeId) so descending always re-seeds from the correct payload rather than showing the parent's nodes.

Canvas rendering. The canvas is a client-only island (the diagramming library is not server-renderable), dynamically imported with SSR disabled; its stylesheet is imported locally rather than globally. Custom node types exist for a Component (title, kind icon, descend affordance, source+target handles) and a read-only boundary proxy.

Client/server boundary discipline. Client canvas code obtains domain types from the tRPC router-output inference helpers (via top-level type-only imports), never from server service modules — otherwise the server graph (Postgres driver → Node built-ins) leaks into the browser bundle. Zod input schemas live in a Zod-only module so client forms can import them as values safely.

Deterministic markdown serialization. Nested (mirrors containment), single round-trippable artifact with project-level YAML frontmatter carrying stable ids. Global sort key: depth → title (raw codepoint) → id. No timestamps in output. A Component's authored documentation is heading-shifted via a markdown AST transform (not regex) so it can't masquerade as structural headings. Output includes per-Canvas Connections and derived boundary context so any subtree export is self-describing.

MCP server. A co-located route handler speaking Streamable HTTP, built on the official MCP SDK via a maintained Next/Vercel adapter (exact package + versions pinned at install). Local stdio use is provided by documenting an existing stdio↔HTTP bridge rather than shipping a second server (and so production database credentials never land on a developer's machine). Auth: Authorization: Bearer token → hash (HMAC with a server pepper) → token lookup (reject missing/revoked/expired) → actor. No tool accepts a userId; identity comes only from the token. Resources return markdown (project, subtree, cheap index); tools return minimal JSON. Tool surface: list/get/serialize (read), create/createChild/connect/updateDocs/move (write), and a transactional apply-graph batch. No delete tools in the MCP surface for the first release (bounded blast radius), gated behind a future destructive scope. Service errors surface to the agent as readable tool-error text, never stack traces.

AI-friendly API documentation. The MCP tool/resource JSON Schemas (generated from the same Zod used for validation) are the primary agent contract. Add an llms.txt discovery entry point, a "Connect an agent" in-app page, and a normative markdown-format spec. No OpenAPI / REST layer in this release (tRPC is not natively REST and there is no non-MCP consumer to justify the second surface).

New dependencies (by milestone): the diagramming library (M1); a markdown renderer + AST toolchain for render and heading-shift (M2); the MCP SDK + Next adapter (M4); token hashing uses the built-in crypto module (no dependency). A dedicated client state library is not needed (the canvas library's store + component state + query cache suffice for single-editor). New environment variable: a server-side token pepper (added to the schema-validated env in both the schema and the runtime map).

Documentation artifacts (created lazily, per repo convention). A root CONTEXT.md with the binding glossary, and Nygard-format ADRs for: explicit canvasNodeId Edge scope; derived/transitive boundary proxies; the (db, actor, input) service layer; deterministic markdown; the canvas-library SSR-disabled island; MCP transport + token auth; capability-URL sharing; and soft-delete + undo.

Sequencing. M0 schema + service layer (no UI) → M1 first nested Canvas (create/drag/connect/rename/descend + soft-delete/undo, with a minimal create-project / get-by-slug path) → M2 markdown export + in-app doc editor → M3 boundary propagation (read-only proxies) → M4 MCP (tokens, route, tools/resources, llms.txt, connect-an-agent) → M5 refinement-edge wiring, auto-layout, sharing polish.

Testing Decisions

This PRD introduces the repo's first automated test harness — Vitest (the natural fit for this TypeScript/ESM/Next stack). There is no prior art for tests in the codebase today (pnpm check — ESLint + tsc --noEmit — has been the only validation gate), so this work establishes the pattern as well as the coverage.

What makes a good test here: assert external behavior, not implementation. For the service layer that means: given a database state, an actor, and an input, assert the returned value and the resulting database state. Tests must not assert on internal query structure, call counts, or private helpers — those are free to change. The (db, actor, input) signature is the deliberate testable seam: tests inject a real database (an ephemeral/local Postgres, isolated per test via transaction rollback or truncation) and exercise the function directly, with no HTTP, no React, and no MCP layer in the way.

Modules under test — the architecture service layer:

  • node operations — create / createChild place Nodes on the correct Canvas; move reparents and rejects cycles (a Node can't become its own descendant); self-edge and illegal-shape Connections are rejected; updatePositions persists batched positions.
  • edge operations — connect validates same-Canvas / boundary legality and de-dupes active Edges; direction/label persist.
  • soft-delete + undo — deleting a Component soft-deletes its entire subtree and incident/Canvas Edges and excludes them from reads; restore brings exactly that set back; nothing outside the subtree is touched.
  • canvas read (getCanvas) — returns exactly the interior Nodes and Edges for a given scope, plus correct breadcrumbs, for root and deeply nested Canvases.
  • access — owner can read and write; a non-owner with the capability slug can read but every mutation is denied; an actor scoped to one user can never reach another user's project.
  • boundary derivation (M3) — boundary(H) returns the correct transitive set down a multi-level subtree (the property that makes the product novel).
  • serializer determinism (M2) — a golden-file byte-equality test: serializing the same graph twice, and under a different locale, yields identical bytes; documentation is heading-shifted correctly; Connections and boundary context appear.

UI modules (the canvas island, tRPC adapters, RSC pages, MCP route) are intentionally not unit-tested — they are thin glue or DOM/library-coupled, and are validated by running the app and by an MCP Inspector round-trip (see Further Notes).

Out of Scope

  • Real-time multiplayer / co-editing (presence, cursors, CRDT/Yjs). v1 is single-editor optimistic; live collaboration is a categorically different architecture and a separate effort.
  • OpenAPI / REST API layer. MCP is the agent interface for this release.
  • In-app AI-native generation (generate-architecture-from-description, auto-document-a-Component, explain-a-subtree). These reuse the same services later; flagged as future, not designed here.
  • MCP delete/destructive tools. No delete tools in the first MCP surface even though soft-delete makes them recoverable.
  • Refinement-edge wiring UI (drawing Connections to boundary proxies). Boundary proxies are read-only through M4; wiring is M5.
  • Auto-layout beyond manual positioning (graph auto-layout is an M5 stretch).
  • Partial-unique-index database hardening for active-Edge uniqueness (service-level de-dupe stands for now).
  • Fixing the stale CareerCraft Studio project overview in CLAUDE.md — tracked as a separate chore.
  • Granular visibility levels (public listing, link-edit, org/team roles) beyond owner + capability-view.

Further Notes

  • Verify by running, not just pnpm check. The validation gate cannot catch a client/server bundle leak or an N+1 in getCanvas. Manual verification for the core slice: create a project, drag a Component and confirm exactly one position mutation fires on drag-stop (no per-pixel storm), connect/rename, descend and confirm the breadcrumb and that the child Canvas shows the child's Nodes (the keyed remount), then soft-delete a Component with children and undo it. The dev-only artificial 100–500ms procedure delay deliberately surfaces waterfalls — but it also disguises N+1s, so measure query behavior with it off.
  • MCP verification (M4): an MCP Inspector (or Claude Code) round-trip — create a Component via a tool, then read the project resource as markdown; confirm an unauthenticated request is rejected, and that a token cannot touch another user's project.
  • Prompt-injection standing note: Component documentation is untrusted input when later fed to an LLM; treat it as data, never as instructions. Record this in CONTEXT.md.
  • Dogfooding / delight: model infinite-docs inside infinite-docs as the canonical demo and as the serializer's golden-file fixture — the product proves itself by documenting itself.
  • The full M0–M5 design (with the agents' rationale) lives in the approved plan; this PRD is its single-issue synthesis.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions