Case study 001, 2026, civic tech
Search and version Peru's laws.
Legalize.pe turns Peru's laws into Markdown files under git, with every reform as a commit dated to the day the norm was published. I worked on the read side, the web app and the API that serve the corpus and its history.
- Role
- Co-maintainer on the read side, credited on the project's About page. I joined after the scraper, the corpus pipeline and the first web app and API were already in place, and I took over the read side: I made the history API work, hardened it, deployed it, and polished the web app.
- Problem
- Peru publishes its laws, but not as data you can search, diff, or trace back through every amendment.
- Decision
- On Vercel there is no clone of the corpus, so I moved the history routes onto the existing git service, which reads from the GitHub API there and from local git when a repo path exists. I bundled the search index into the function instead of reading it from disk.
- Result
- The web app is live and open source, still pre-1.0, with 21,244 norms in the corpus. I fixed the history endpoints that returned 404 on every request, and one hardening pass fixed seven QA findings.

Context
What the project is
The project has two repos. The corpus repo is plain Markdown plus git history, so running git log on the 1993 Constitution replays its 32 amendments, and anyone can clone the raw data. The engine repo is a Turborepo monorepo with the scraper, a CLI, the Astro web app, an API, and an MCP server so AI clients can query the law. The README lists 21,244 norms, 11,045 national and 10,199 regional across 26 jurisdictions.
Build
Making the history API work
A QA review found that every version endpoint returned 404. Two bugs caused it. The routes read git from the engine repo, which has no norms, and the reader looked under leyes/pe/ while the corpus uses pe/. I fixed the path and moved the routes onto the existing createGitService() factory. On Vercel there is no clone of the corpus, and that factory reads from the GitHub API there, so history works in production. The same PR filled the gaps around it: the missing /compare route, title matches ranked above body matches in search, GITHUB_TOKEN support that raises the GitHub rate limit from 60 to 5,000 requests an hour, and docs for all 10 endpoints.
Build
Fixing the serverless crashes
The smart search endpoint returned a 500 in production. It read a 5.3 MB search index from public/ at request time, and on Vercel that folder lives on the CDN, not inside the function. I imported the index through a Vite alias so the bundler puts it in the function, the same pattern the repo already used for its id-to-path map. The OG image route had the same bug and was serving one generic image for every law. Two smaller fixes followed. GET params now come from request.url, and the API routes are excluded from ISR.
Build
Hardening and deploying
A QA sweep left seven findings, and I fixed them in one PR. Git endpoints now resolve the real file path and return a clean 404 for unknown ids. Malformed JSON gets a 400 instead of a 500, and unsupported methods get a 405 with an Allow header. About 1,000 titles had HTML entities, which are now decoded when the index is built, and the patch parser moved into one shared module with its line counting fixed. Separately, I deployed the standalone API to Vercel against a Turso database. In production QA its stats endpoint timed out because it sorted on an unindexed column. Adding the index took that query from 13 seconds to 1 second.
Build
The web app
First, I added a Spanish and English toggle. It translates the interface only, never the law text. A small inline script applies the language before the first paint, so the page does not flash. The rest was repair and polish. I fixed a command palette that crashed on open, got CI passing on main again, and checked the app against the Vercel Web Interface Guidelines, which led to fixes in nested button and link markup, focus rings, reduced motion, and a shortcut hint that matches your platform.
Decision
Reuse what the repo already had
Both serverless fixes reused code that was already in the repo. The git service already had a GitHub API backend, and the search index fix copied the way the repo bundled its id map. That kept the change small and added no new storage service. In the hardening PR I listed rate limiting, canonical-name resolution and two other items as out of scope, because they needed new infrastructure or verified legal data.
Context
Result
The web app is live at legalize-pe.crafter.ing and the search endpoint I fixed works. The engine and the corpus are open source, and the README still marks the project pre-1.0, so expect breaking changes. The standalone API deployment is not live today. The API routes inside the web app are.
Next case
Mortgage AI