Skip to work
← All work

AGILA CATalog

An animal welfare organization's cat records moved off a strained spreadsheet without anyone having to stop using the spreadsheet.

Aug 2025 – present

cat records
500+cat recordsdigitized and kept in sync
org members
40+org membersreleased from a manual workflow
migration disruption
0migration disruptionexisting workflows never broke

The problem

AGILA tracked more than 500 cats across campus. Health status, neutering history, vaccination records, adoption eligibility: all of it spread across a shared spreadsheet, Google Forms and a Facebook page. It worked, in the sense that the org had run on it for years, but the cost landed on more than 40 volunteers: duplicate rows, competing versions of the same file, and no reliable way to answer a question about one cat. Adoptable cats were effectively unfindable by anyone outside the org. I found the problem myself, pitched it, and led a four-person team to fix it.

The constraint that decided everything

The obvious build is a database with an admin panel, then a migration weekend, then training. I did not believe that would survive. The spreadsheet was not just a storage format for these volunteers. It was the workflow they trusted, edited on phones between shelter visits, shared with people who would never log into an internal tool. A cutover would have meant asking 40 volunteers to change how they work in exchange for a promise. Most would have quietly kept using the sheet, and then we would have had two sources of truth instead of one.

The engine

So the sheet stayed, and the database became the source of truth underneath it. A Cloudflare Worker fires every twenty minutes and runs a fixed sequence: reconcile, reverse sync from sheet to database, import photos, forward sync back out, regenerate summaries. Two ordering decisions carry most of the weight. Reconciliation runs before the tick’s idle exit, because forward sync is task-driven, so nothing would otherwise notice a cat whose row had gone missing. And reverse runs before photo import, so a cat created from a new sheet row exists before the importer tries to attach its photo.

One column holds it together

Column Y of every region sheet holds a UUID that is the cat’s database primary key. Not a foreign key into a mapping table: the key itself. A row without one is invisible to sync, which makes that failure loud instead of silent. An Apps Script trigger stamps the editor and the edit time into the two columns beside it, and that timestamp is how the engine tells a human edit from its own write. Conflicts resolve last-edit-wins, with a five-second buffer favouring the database.

Two rules I would defend in a review

A blank health cell imports as unknown, never as healthy, and vaccination reads Yes, Expired or Unknown, because a shot from two years ago is not protection. On a page where strangers decide whether to take an animal home, missing data must not become a false medical claim. Second: the sync can be retired, not only paused. Retiring releases the app’s column protections and leaves AGILA a spreadsheet it can still edit, rather than one locked by software nobody maintains. A student project should be able to end without taking the org’s data with it.

The outcome

AGILA runs on it now, at ateneanagila.com, with the adoption catalog public and indexed. The records are structured and queryable, the manual reconciliation is gone, and the volunteers’ day did not change, which was the point. The engineering that mattered here was not the CRUD. It was deciding that the old system had a right to keep existing.

Screenshot

The AGILA CATalog admin database: 561 cats on record, each card carrying the cat's name, breed, region and adoption status, with a link straight back to the org's Google Sheet.
The AGILA CATalog admin database: 561 cats on record, each card carrying the cat's name, breed, region and adoption status, with a link straight back to the org's Google Sheet.

More work