Nicolás Duque

Patas Arriba Travel: pet exports, from discovery notes to production in 16 days

Sep 22, 2026 · 12 min read
Client
Patas Arriba Travel — veterinary clinic, Colombia
Role
Solo: discovery, product, architecture, code, infrastructure
Period
September 2026 — ongoing
Stack
Next.js 16 · React 19 · Postgres 17 · Kysely · AWS · Terraform
Live
patasarribatravel.com

Exporting a pet from Colombia to the United States takes months. A vaccine, then a 30-day wait, then a blood sample that travels to an approved laboratory abroad, then a result, then a health certificate, then the ICA export permit, then a flight with a booked slot. Miss a window and the trip moves by weeks.

Patas Arriba Travel is a veterinary clinic that runs that process for its clients. Three actors touch every case: the clinic's team, the pet's owner — the tutor — and the vet doing the visits. When I met them, the whole thing was held together by WhatsApp threads, email and files on a shared drive, and the team was spending about five hours a day answering one question: how is my case going?

They had 35 to 40 active cases at once.

The system's landing screen, titled What needs doing today, listing a complete batch of three samples ready to ship on Wednesday, a laboratory result that should already have arrived, and a veterinary assessment not yet logged
The screen the team opens in the morning. Not a list of cases: a list of what will go wrong if nobody moves today.

The screenshots in this case study come from the system running with its built-in sample data — six fictional cases the app can create and delete on demand — so no client information is shown. The interface is in Spanish, which is the language the clinic works in.

The decision that shaped everything: don't redesign the process

The clinic's workflow already worked. It was in people's heads and in a Drive folder, but it worked — they had exported animals for years. So the project was never "design a better process". It was systematize the one that exists.

That sounds modest. It is actually the decision that removes the most common failure in this kind of software: building for an imagined process. It also relocates the source of truth. The spec doesn't come from me interviewing them once; it comes from the operations team, continuously, and my job is to turn what they say into rules a machine can execute and tests can check.

Which means the hard part is not the code. It's getting months of tacit knowledge out of three busy people who have animals to attend to.

The async system with the client

I never asked them to read a spec. I asked them a long list of small questions.

The client-facing board has one section per block of the process, from intake to exceptions, and inside each section one numbered item per thing I needed to know. Every item says what to deliver and roughly how long it takes. The board's description sets the contract:

  • The whole thing is about 6–8 hours of work, splittable across several people.
  • Deliver into a shared folder, one subfolder per block.
  • Photos and screenshots are fine. Nothing needs to be typed up.
  • If something doesn't exist, write "it doesn't exist" — that is valid information, and often the most valuable kind.
  • Never send passwords by chat or email.
  • Two deadlines, a week apart: first the items that block the build, then everything else.

That last point is the one I'd defend hardest. One deadline for everything means nothing arrives until the end; splitting it meant the blocking answers came back while I still had a week of work that didn't depend on them.

Over the following two weeks they worked through most of the list. Every answer landed, item by item and dated, in a single file in the repository: docs/10-respuestas-de-operaciones.md. That file is the source. When something in the code looks arbitrary six months from now, the reason is one grep away, with the date and the person who said it.

The loop: the clinic answers items on a shared board, answers land in one dated file in the repo, the file becomes a versioned rulebook, the rulebook is replayed against three real closed cases, CI gates the merge and the change ships the same day
The path from a question in a board to a rule running in production.

The part I'd keep in every project: an unanswered question is a named blocker, not a vague delay. Questions that stayed open got their own entry, and each one was filed in the development board as a client dependency that states exactly which feature it blocks. Nobody has to remember why a thing isn't done. The board says: this is waiting on that answer, from that person.

A second Asana project mirrors the development plan, task for task, with the same MVP-nn codes used in the repository. Three deliveries, each usable by the clinic on its own, each with an exit criterion instead of a date:

A delivery ends when its criterion is met, not when the week ends.

For delivery one the criterion was: the engine reproduces three real closed cases within tolerance, and 100% of active cases are loaded with their real dates.

The core: a requirements engine, not a CRM

Everything else hangs off one component: a versioned rulebook per destination. What each country requires, in what order, with what time windows, per species.

From it the system generates each case's checklist, target dates and alerts:

A case file showing the steps of a dog's export to the United States: veterinary assessment, rabies vaccine, microchip, blood sample and laboratory shipment marked as done with their real dates, the antibody result marked as can be done, and the remaining steps blocked with provisional dates, each with its responsible party
One case, generated by the rulebook. Done steps carry the real date; the rest carry a target and who owns it — the team, the vet, the tutor or the laboratory. Anything downstream of an unknown says "provisional".

The scheduling is not a simple forward chain:

  • It schedules backwards from the flight for the final leg — documentation 20 days out, health certificate 10, ICA permit 5 business days — and forwards from the clinical facts for everything else. With no flight booked, the final leg isn't invented: the plan says "from this date onward".
  • Prerequisites carry their own waiting time: the blood sample needs vaccine + 30 days and microchip + 0.
  • Requirements can be marked by species, which matters more than it sounds.
  • The laboratory shipment isn't calendar-driven. It goes when a batch of samples is complete, so the planner takes the state of the queue as an input and returns a range when there's no batch yet.

That last one was a finding, not a requirement. I had modelled the shipment as a weekly date; operations explained it goes out when there are three or four samples to send. So the batch got its own screen, and its own unit of waste:

The samples screen: three samples waiting for a batch with the days each has been waiting, a note saying the batch is complete and adds up to 22 tutor-days of waiting, a form to register the shipment, and below it a sample already sent without a result 34 days ago
22 tutor-days of waiting is the number that made the batch rule visible. Every day a sample sits in the queue is a day a family waits, so the screen adds them up instead of hiding them.

Same story with the "paperless" assumption: there is a paper form, the vaccination card, and the visit screen now mirrors its structure so the vet types what they already write.

Proving the rules against reality

Rules extracted from interviews are a hypothesis. So the engine ships with a replay harness: three real closed cases, anonymised, in the test suite. Every rulebook change is replayed against them, and the test fails if a case stops reproducing.

The first run was the most useful hour of the project. The engine got the clinical phase right — in the European case the 30-day wait landed on the exact day — and got the final leg wrong, which is precisely where the rules had been guessed rather than told. Two decisions already taken were reversed on the spot.

The harness runs in CI on every push, next to the typecheck, the app's own tests, and the migrations applied to a clean Postgres. Red blocks the merge.

Then the rule that best shows why this is worth the trouble: on 21 September the clinic confirmed that cats travelling to the USA don't need serology. That single answer removes the sample, the shipment and the lab wait from a cat's plan: nine weeks become three. Rulebook updated, replay green, deployed the same day.

Working this way with Claude Code

I write this software with Claude Code, and the leverage doesn't come from typing prompts faster. It comes from the substrate the agent works on, which is exactly the substrate a new engineer would need:

  • Eighteen numbered documents carry the project: vision, domain model, implementation plan, decisions, open questions, the answers file, the replay report, the MVP proposal, the development plan, infrastructure. The agent reads those, not a chat history.
  • Decisions are ADRs (D-001D-017) with a status and, deliberately, a way back. "One web app instead of a separate API" is written down as a decision that can be reverted, not as an accident of the first sprint.
  • The domain language is Spanish, everywheremotor-requisitos, casos, muestras, lote, bitácora. The words in the Asana task, the words in the answers file, the words in the schema and the words the clinic says on the phone are the same words. Translation layers are where meaning goes to die.
  • Docs ship with the code. The commit log alternates feat and docs because a change that isn't written down is a change the next session won't respect.
  • The tests are the guardrail that makes speed safe. An agent can move very fast through a codebase where three real cases must keep reproducing and CI refuses to merge red.

Fifty-one commits between 6 and 22 September. The internal application was live on 14 September with its secrets in Parameter Store; after that, most days ended with something deployed.

What shipped

For the team, an internal area: what needs doing today as the landing screen, the case file with its calculated plan, the sample queue and batch, visit logging with the structure of the vaccination card and photos as evidence, barcode scanning, a CSV importer that takes the same columns as the spreadsheets they already keep, and a measurement page.

For the tutor, a page with no password: a revocable per-case link showing where the case stands, what they need to do and what's already done, plus document upload — after consenting to data processing, with version, channel, date and IP recorded, because Colombia's Law 1581 applies.

Two phone-sized views of the tutor page: the first shows the pet's name, the date it can travel, a what-you-need-to-do block with flight, airline booking and ICA certificate, and a what-comes-next list; the second shows the already-done steps with their dates and a consent box naming the data policy version before documents can be uploaded
What the tutor sees, on the link the team pastes into WhatsApp. Three sections and no jargon: the date, what's theirs to do, what's coming, what's done. The consent box is what turns a legal requirement into one tap.

That page is the whole business case. Every tutor who reads it instead of writing "any news?" is a message the team doesn't answer.

Details that decide whether software gets used:

  • The visit form works without signal. It saves the visit and its photos on the device and queues them; they leave on their own when the connection returns, without duplicating. The vet is in a consulting room, not at a desk.
The visit form: service date, service type and attending professional, a microchip block asking to verify the reading on every visit with a scan-with-camera button and the number on file shown below, and a vaccine block with brand, batch, vial expiry and next dose, labelled as on the vaccination card label
The visit form follows the paper card field by field, including the vial batch. The microchip is re-read on every visit — the barcode scanner is in the browser — because a swapped chip discovered at the airport is a trip lost.
- **Tutor isolation is enforced by the database**, not only by the app: everything the tutor's session touches runs as a restricted Postgres role with row-level policies, so a query that forgets a filter still returns nothing. Verified in CI with a non-superuser owner, the way RDS actually behaves. - **The internal area works on a phone** — collapsible menu, tables as cards, 44px targets, no zoom jump on iOS. - **Sample data and a guided tour.** An admin creates six fictional cases in different stages to practise on, and deletes them in one click. Every screen has a Help button with a step-by-step guide that points at real elements, not at the demo data.

Infrastructure, all in Terraform: one ARM EC2 instance with Caddy and Docker, Postgres 17 on RDS, a private S3 bucket for documents, SES for access links and mail, Route 53, secrets in Parameter Store. Around US$35–60 a month, nightly backups, and a restore drill as a script instead of a promise.

Two entry points, a single Next.js application on one EC2 instance with Caddy and Docker, talking to Postgres on RDS with row-level security, a private S3 bucket and SES, all described in Terraform
One application, one instance, one database. A clinic with 40 cases doesn't need a platform, and the money it doesn't spend on one is the money that keeps the project alive.

Where it stands, honestly

The system is in production at patasarribatravel.com — the internal area behind /interno, the public root still showing a coming-soon page while the landing is reviewed with the client. A handful of questions are still open, mostly around lead capture (which needs the WhatsApp Business API) and pricing.

And the number that matters — five hours a day — has not moved yet, because the clinic hasn't started working inside the system. That's the next milestone, and it's deliberately not a technical one: the baseline is written down, the exit criterion for this phase is the team working from the board and freezing the Drive, and the measurement page already counts what it can count on its own.

The measurement page: counters for active cases, tutors with a link, percentage of links opened in the first week against a 60% target, documents sent by tutors, data authorisations, tutor-days waiting for a batch, dates logged in the last seven days and visits logged; below, a form to write down by hand the hours per day spent answering tutors
Half of this page counts itself. The other half is a box where the clinic writes down the hours, because the number that justifies the project is the one no system can observe.

I'd rather show a case study with that sentence in it than one with an invented percentage.

What I'd take to the next project

  1. Break discovery into small, scoped asks instead of one big document. How many, and how they're grouped, depends entirely on the business — what travels between projects is the shape: one item per question, what to deliver, how long it should take, and permission to answer with a photo.
  2. Make "it doesn't exist" a valid answer. Half the value came from what they told me wasn't there.
  3. Replay real closed cases before building screens. It cost one day and reversed two wrong decisions.
  4. Exit criteria instead of dates, especially when part of the input comes from someone else's calendar.
  5. Name your blockers. A question tied to the feature it blocks moves. A question in a chat thread does not.