Inspiration
Every engineering team knows the feeling. A feature ships, the code changes, and somewhere in a docs folder or a wiki sits a page that silently becomes wrong. Nobody updates it because nobody owns it, and nobody owns it because the tooling makes it too slow and too manual to keep up. Doc rot is not a new problem, but it has become a worse one as codebases move faster and teams stay lean.
We wanted to build something that treated documentation the way continuous integration treats code: as something that should be checked automatically, flagged when it breaks, and fixed without requiring a human to remember to do it. The inspiration was simple frustration with the gap between how much care teams put into their code and how little infrastructure exists to keep the words describing that code honest.
GitHub already captures every meaningful moment of change through commits and webhooks. We asked whether it was possible to wire an AI into that event stream and have it take responsibility for documentation updates the same way a bot takes responsibility for running tests.
What It Does
DocMed connects pairs of GitHub repositories: a source repository where code lives, and a documentation repository where generated docs are stored. When a developer pushes a commit to the source repo, DocMed receives a webhook event, filters the changed files down to those worth documenting, and sends them through an AI pipeline that produces or updates structured documentation for each file.
The output lands in the doc repo as a pull request. Engineers can review it, merge it, or ignore it. The docs stay close to the code without anyone having to write them manually.
A companion viewer application, generated alongside the doc repo, renders those docs as a searchable, tree-structured site with tabbed navigation, light and dark themes, breadcrumb navigation, and a commit history view. The viewer deploys directly from the doc repo, so documentation always reflects the latest merged state.
On the product side, users connect their GitHub accounts through a GitHub App installation, create repo pairs through a dashboard, and manage their profile and billing from a settings page. Everything is scoped to the user, persisted in Firestore, and driven by GitHub App webhooks so no polling is required.
How We Built It
The stack is centered on Next.js App Router for both the frontend and the backend API routes. Authentication is handled by Clerk, which manages sessions, user profiles, and identity. GitHub integration runs through a GitHub App, giving us fine-grained repository access, webhook delivery, and installation-level tokens without requiring users to generate personal access tokens manually.
When a push event arrives at the webhook endpoint, the server resolves the installation token for the repository, reads the changed files from the GitHub API, and filters out anything that should not be documented: binary files, minified assets, machine-generated files, config formats, and lock files. The remaining files are queued as individual jobs in QStash, which provides durable delivery and retry logic without requiring us to run a background worker process.
Each queued job fetches the file content, constructs a prompt, calls an AI model, and writes the result back to Firestore as a structured doc entry keyed to the file path. A separate process assembles a manifest from those entries and pushes everything to the doc repo as a PR.
The doc viewer is a Vite-based React application that lives in the doc repo template. It reads a JSON manifest and lazy-loads individual doc files on demand. The file tree is built from the manifest keys at runtime using a recursive tree builder that uses double-underscores as path separators, matching the key format produced by the pipeline.
Firestore stores user data, repo pair configurations, GitHub installation records, and the generated doc files. Installations are kept in their own subcollection so that disconnecting a repo pair does not remove the connected GitHub account, which would break other pairs sharing the same installation.
Challenges We Ran Into
The GitHub App authentication surface is more complex than it appears. Installation tokens, app-level JWTs, and user OAuth tokens each have different scopes and lifetimes. An early version of the account picker showed every connected account as "Unknown" because we were calling the wrong GitHub API endpoint with the wrong token type. Resolving account names required switching to an app-level JWT and calling the installations endpoint directly, which is not the obvious path from the documentation.
Webhook delivery at scale introduced a subtler problem. The pipeline initially queued every file in a pushed commit, including lock files, generated assets, and dependency manifests that are hundreds of kilobytes of noise. This inflated job counts, wasted API calls, and produced useless documentation. Building a reliable filter required iterating on the list of skippable extensions and path patterns, and moving that filter upstream to run before any jobs are enqueued.
The doc viewer tree had a path separator mismatch that caused the entire folder structure to collapse flat. The pipeline was writing keys with double-underscore separators but the tree builder was splitting on forward slashes, so every file appeared at the root with no nesting. Tracing this required reading both sides of the pipeline and realizing the convention had drifted between the writer and the reader.
Keeping GitHub installations in sync with the UI required a dedicated Firestore subcollection. Before that, installations were inferred from existing repo pairs, which meant deleting a pair could make a connected account disappear from the dropdown silently. The fix required migrating to an independent record that persists until the user explicitly removes it.
Accomplishments That We Are Proud Of
The end-to-end pipeline working without a persistent server process is something we are genuinely proud of. From webhook receipt to pull request creation, every step runs as a stateless function or a queued job. There is no server to keep alive, no cron job to monitor, and no worker process to crash. The architecture is boring in the best way.
The doc viewer's combination of collapsible folder tree, tabbed navigation, breadcrumb traversal, and dual theme support came together cleanly into a self-contained deployable application that reads a single manifest file and builds the entire interface from it. It requires no backend and no build step in the doc repo itself.
The GitHub App integration being fully installation-based rather than personal-access-token-based means DocMed works correctly in organization contexts where PATs are restricted or rotated frequently. That decision, which cost significant effort to get right, makes the product meaningfully more useful for the teams that need it most.
What We Learned
GitHub Apps are the right abstraction for repository-level integrations but the documentation for them is scattered across several guides that each assume different starting points. The difference between what an installation token can access and what an app JWT can access is important and easy to get wrong the first time.
Filtering at the entry point of a queue is dramatically cheaper than filtering after the fact. The improvement in pipeline behavior after moving the file filter before the QStash enqueue was immediate and significant. Queuing is not free, and every job that enters a queue should be a job worth running.
Building a UI that looks intentional requires making explicit decisions about every surface rather than accepting defaults. The Clerk UserProfile component, the sidebar, the landing page sections, and the doc viewer all needed deliberate theming passes before they felt like parts of the same product rather than components from different libraries placed next to each other.
State that lives in only one place is much easier to keep consistent. The problems with GitHub installation records, account names, and UI sync all traced back to the same root cause: installation state was being inferred from pair records rather than stored independently. The fix was simple once the source of truth was clear.
What Is Next for DocMed
The immediate next milestone is billing integration, which will gate access to additional repo pairs and higher documentation throughput. The payment tab in settings is already in place as a placeholder, and the backend pair limits are designed to accommodate plan-based quotas.
On the documentation quality side, the AI pipeline currently treats each file independently. A meaningful improvement would be cross-file context: understanding that a function called in one file is defined in another, and producing documentation that reflects those relationships rather than describing each file in isolation.
Support for monorepos is a natural extension. Many of the teams most likely to have doc rot problems work in large monorepos where different subdirectories are owned by different teams. Routing documentation to team-specific doc repositories based on file path patterns would make DocMed more useful in those environments.
The doc viewer could also support versioning, allowing readers to navigate documentation for a specific release tag or branch rather than always seeing the latest state. The manifest format already includes enough information to support this with relatively modest changes to the pipeline and the viewer.
Longer term, the pull request workflow could be supplemented with inline suggestions posted as review comments on the original code PR, so engineers see documentation feedback in the same place they see code feedback, without switching contexts.
Built With
- claude-code
- gemini-api
- next.js
Log in or sign up for Devpost to join the conversation.