Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.
Supersedes or amends: ADR-007
Date: 2026-10-20
Status: Accepted
Deciders: Daniel Meza
Supersedes: ADR-007
Related: ADR-012 — Python FastAPI for the API, ADR-016 — Lightweight admin auth, Architecture reference
Context
ADR-007 chose a hybrid ingestion pipeline: scheduled scrapers and PDF parsers per issuer, plus manual CSV upload, run by a separate ingestion service that wrote canonical product rows and tracked failures. Its premise was that Costa Rican banks publish no machine-readable feeds, so data would be collected automatically and editors would only override.
Since then the CMS shipped (FEAT-018 to FEAT-024): editors create and edit issuers, products, editorial reviews, offers, articles, and guides through the admin UI and /api/v1/admin/**. That became the real data path. The ingestion service stayed in the repository, but nothing in the codebase calls its API proxy, the local dev stack (pnpm dev:local) does not start it, and editors never interact with it. Meanwhile it cost maintenance: a second Python service, its tests, a proxy in the API, an ingestion_runs table, and documentation for sources nobody scraped.
Decision
Product data is created and updated only by editors through the CMS. There is no ingestion service, no ingestion API, and no ingestion bookkeeping in the database.
- Delete
apps/ingestion, the/api/v1/ingestion/**proxy, and the repository methodsrecord_ingestion_failure/record_ingestion_success. - Migration
0010dropsingestion_runs, theingestion_sourceenum, and thefinancial_productscolumnslast_attempted_at,ingestion_failure_count, andis_stale. - Freshness is derived from age.
is_stalestays in API responses and in the web app's freshness chip, but it is computed on read: a product is stale whenupdated_atis older thanSTALE_AFTER_DAYSdays. The default is 45, chosen because issuers publish rate sheets about monthly, which leaves editors about two weeks of grace. An invalid value fails the API at startup. A CMS save resetsupdated_at, so editing a product clears the flag.
Considered alternatives
- Keep ingestion, unused. Costs maintenance and misleads readers of the docs about how data enters the system.
- Make
is_stalea manual flag in the CMS. Depends on editors remembering, and does not tell them which cards need attention. - Remove
is_staleentirely. Simplest code, but it removes the visible stale chip from the disclosure layer (FEAT-014). - Compute staleness in SQL. Spreads the rule across every query that builds a product. One small function is easier to test and change.
Consequences
Positive
- One data path, one place to look: editor saves are visible on public pages immediately.
- One service fewer to deploy and secure (Railway
ingestionis deleted in staging and production). - The stale chip now says something true that editors can act on.
Negative / to watch
- Data accuracy depends entirely on editor diligence. There is no automatic detection that an issuer changed a rate.
- On the first deploy every product not saved in the last 45 days shows the stale chip. Count them before promotion and re-save the ones that are current.
- If automated collection ever returns (for example a bank publishes a feed), it should come back as a new ADR. It would write through the same CMS-facing repository, not a parallel service.
- The CMS product editor only covers credit cards (
/api/v1/admin/products/tarjetas), so pension fund and home loan data have no maintenance path yet.
Deployment order
Migration 0010 is destructive. Deploy the API first (it no longer reads the dropped columns, so it works against both schemas), then run the migration, and snapshot the database before each production change.