Hacer Aldea
A shared map of what is happening in a small town and who is doing it, open from a link, a QR code or a WhatsApp message, without an account. Built as one TypeScript system that a coding agent can hold in its head.
Context
VerifiedIn the small Mexican towns this was designed for, community life already works: WhatsApp groups, flyers, Instagram, word of mouth. What does not work is legibility. Information is duplicated, disappears within hours and is invisible outside your own groups.
The product plan makes four promises:
- A visitor knows what is happening near them today within ten seconds.
- An organiser says it once, in WhatsApp, and it stays findable.
- A local steward keeps their town legible in twenty minutes a week, from a phone.
- A newcomer understands the place in one evening.
It is explicitly not a social network, a marketplace, a tourism guide or a chatbot. AI only removes friction inside deterministic workflows.
Constraints
Verified- No accounts for visitors. Everything public opens from a link or a printed QR code.
- Previews matter more than pages. Links travel through WhatsApp, so each one has to preview with the real event, not a blank app.
- Many towns from the first commit. One network, several communities, each in its own time zone, with no data leaking between them.
- WhatsApp's own rules. A business number cannot read groups, so the flow is forward-to-publish. Business verification and message templates take weeks and cannot be hurried by code.
- Only publish what a source made public. Seed data carries no phone numbers, because a plausible Mexican mobile is somebody's real number.
- Agents write much of the code. One language, one process, one schema, and boundaries enforced by tooling, so an agent can hold the whole system in context and cannot quietly cross a line.
My part
VerifiedThis is my own project. I shaped the product, made the architectural calls recorded in its decision log, and directed the Claude Code agents that wrote most of the code. I reviewed what merged. All 50 commits to date come from my studio machine.
The repository's AGENTS.md sets the rules the agents work under: change a documented decision only by writing a new one, never spread a database row into a response, and treat pnpm check (types, lint, format, migration drift and tests) as the definition of done.
Architecture
A modular monolith in a pnpm workspace. Select a box for its rules, or trace a request through the system.
Verified boxes and boundaries Illustrative layoutServer
- Does
- Serves the public JSON API under /api and the web build, injecting each route's Open Graph tags so shared links preview properly.
- May use
- packages/core and packages/db.
- Never
- Imports the app. Spreads a database row straight into a response: every response goes through an explicit DTO mapper.
- Why
- Link previews are a must, and a static single-page app cannot produce them, so the server renders the meta tags.
Decisions and trade-offs
From the project's decision log, in my words. Open one to see what it cost and what lost.
VerifiedADR-025
Recurring events expand in wall-clock time, not UTC
- Problem
- A weekly class is “Wednesdays at 18:00”, a calendar fact. Adding 7 × 24 hours to a stored instant is right until a clock change, then every later occurrence is an hour wrong. It also quietly depends on the machine's time zone, so CI, the container and a laptop could disagree.
- Decision
- Expansion steps calendar dates in the community's own IANA zone and converts each one to an instant. Nothing reads the ambient time zone. “Until” is stored as a calendar date, not a timestamp.
- Cost
- A class keeps its duration across a clock change rather than its end time. The difference is one hour, once a year, in a country that stopped observing DST in 2022.
- Rejected
- Storing occurrences as UTC instants and stepping them; it fails west of Greenwich and twice a year.
ADR-028
The atlas owns places; this app mirrors them read-only
- Problem
- A separate, already-launched atlas site had curated places, promised URLs and an owner claim flow. This app had the recurrence engine and installability. The same café edited in both would diverge within a month.
- Decision
- Keep both, give places one writer. A nightly job copies the atlas feed in. A database trigger refuses any edit to a mirrored row unless the sync is the one writing.
- Cost
- Places can be up to a day stale, and the public feed has no descriptions or photos, so mirrored places show a name, a kind, a point and a link out.
- Rejected
- Merging the two, which would have thrown away either the launched site or the engine. A live proxy, which would have broken joins and added a network hop to the busiest screen.
ADR-027
Database tests run against real Postgres, and CI proves they ran
- Problem
- The schema is the thing under test. A mock cannot catch a missing constraint, a cascade that does not cascade, or a transaction that leaves an orphaned row.
- Decision
- Tests apply the migrations to a real Postgres and truncate between tests. They skip locally without Docker, but a dedicated test fails in CI if the database URL is missing, so a suite cannot stop running while the pipeline stays green.
- Cost
- Slower tests and a database in CI. A migration-drift check runs too; its first version passed against a schema it should have rejected, so it is now verified in both directions.
- Rejected
- Mocked repositories, which only prove the mock agrees with itself.
Architecture
Link previews need a server, so the web build is served, not hosted
- Problem
- Most links will be shared in WhatsApp groups. A link that previews as a blank app is a link nobody taps. A static single-page app cannot give each event its own title and image.
- Decision
- The same Hono process that serves the API serves the Expo web build and injects each route's Open Graph tags.
- Cost
- A long-running server instead of free static hosting. It also suits webhooks and cron, which the plan needs anyway.
- Rejected
- Static hosting on a CDN with a generic preview for every page.
Architecture
A small typed fetch wrapper instead of Hono's end-to-end client
- Problem
- The plan called for Hono's typed client. In practice the type-only import dragged the server's whole module graph, Drizzle and Node built-ins included, through the mobile bundler just to erase it.
- Decision
- The app declares its response shapes beside a small fetch wrapper, next to the server's DTO mappers that produce them.
- Cost
- Two places to keep in step. Revisit if they drift in a way tests do not catch.
- Rejected
- Keeping the typed client and fighting the bundler.
Try ADR-025
The same weekly class, expanded two ways. Switch to a zone with a clock change, or read the end date the wrong way, and watch the difference.
| # | Stored instant + 7 × 24 h | Stepped in wall-clock time | Instant stored (UTC) | Shown? |
|---|---|---|---|---|
| 1 | Wed 21 Oct, 18:00 | Wed 21 Oct, 18:00 | 2026-10-22 00:00Z | yes |
| 2 | Wed 28 Oct, 18:00 | Wed 28 Oct, 18:00 | 2026-10-29 00:00Z | yes |
| 3 | Wed 4 Nov, 17:00 | Wed 4 Nov, 18:00 | 2026-11-05 01:00Z | yes |
| 4 | Wed 11 Nov, 17:00 | Wed 11 Nov, 18:00 | 2026-11-12 01:00Z | yes |
| 5 | Wed 18 Nov, 17:00 | Wed 18 Nov, 18:00 | 2026-11-19 01:00Z | yes |
| 6 | Wed 25 Nov, 17:00 | Wed 25 Nov, 18:00 | 2026-11-26 01:00Z | yes |
Adding a fixed week to the stored instant shows 4 of 6 classes an hour off after the clocks change. Stepping calendar dates in the community's zone keeps every one at 18:00. The end date is compared as a calendar day in the community's zone, so the last class is kept.
What was built, and when
From the commit history. Two short bursts, a month apart.
Verified- 1 Sep 2026
Workspace, the master plan frozen at v1.0, core rules and time handling, schema and first migration.
- 2 Sep 2026
Occurrence materializer, public API, the app, link previews and a smoke script. Search, calendar files, QR codes, i18n, submissions and stewardship. Docker image and a public deploy.
- 3 Sep 2026
Audits against the existing atlas, then the read-only mirror (ADR-028).
- 10–12 Oct 2026
Guest-facing screens, flyer submissions stored for review, over-the-air updates and a TestFlight build lane.


Result
VerifiedA pilot deploy is running with seeded sample content. On 16 October 2026 I loaded it, and the server returned the community's own title and description in the page's link-preview tags. The last recorded local run passed 57 database tests, 120 server tests and 9 ingest tests. A debug iOS build runs in the simulator, and the TestFlight lane exists.
Not done
- Member accounts, private responses and agreements.
- Persistent listings and notices, and the steward queue for them.
- The WhatsApp intake changes on the live bot.
- Native notifications, store builds and a production launch.
What I would do differently
InferredPick the system of record before the second surface exists. By day three there were two Hacer Aldeas holding places. ADR-028 settled it cleanly, but it is a decision I would rather have made before writing the places table.
Test the contract between repositories, not only each repository. The flyer bot was posting to an ingest route that did not exist yet on this side. A contract test shared by the two would have caught it the day the bot was written.
Start the slow, non-engineering work on day one. WhatsApp business verification and template approval run in sequence and take weeks. The plan itself names them as the longest non-engineering dependency, and no code shortens them.