Skip to content
CTO · 2026

Paaw

Pet adoption platform connecting adopters with 4,000+ shelters

  • Next.js
  • React
  • TypeScript
  • Node.js
  • MongoDB
  • Stripe
  • Vercel AI SDK
  • OpenAI
  • Cloudflare
  • AWS App Runner
  • ECS
  • Postmark
The Adoption Journey

Click any step to see what I built there

Contact a shelterThe core conversion
Found and fixed a client-side state race that silently dropped free contact requests before they reached a shelter
Express Contact: a paid tier whose checkout runs end to end on the server
A deliverability gate that catches unreachable shelter inboxes before an adopter is asked to pay
Fixed
silent loss of free contact requests

Overview

Paaw is a pet adoption marketplace connecting adopters with shelters and rescues in the United States, France, Spain and Belgium. More than 4,000 shelters list their animals for free, and over 120,000 pets have found homes through Paaw since 2022.

I joined as CTO in July 2026 and work across the whole platform: the adopter journey from the first search to the first week at home, the tools shelters use to list animals and respond, payments, Oscar (Paaw’s AI adoption assistant), and the reliability and infrastructure underneath. Since joining I’ve merged 264 pull requests into the web app, I’ve also shipped changes across six other services and tools, from the data API and auth to the scheduled jobs.

264
pull requests merged into the web app
7
repositories, from the web app to scheduled jobs
0 → 340+
test files in the web app, behind a CI gate
120K+
pets adopted through Paaw since 2022

The Problem

Adoption is a funnel that leaks at every step. Adopters have to find a pet that is actually nearby, trust the listing, and reach a shelter that will reply. Shelters are understaffed, and a slow reply can lose an adopter. And the moment after “message sent” is where most platforms go quiet. Underneath all of that, the site had to stay fast under heavy search traffic, and when I started it had no automated tests and no CI gate.

What I Built

  • Faster, smarter search: radius-over-map search, live filters, city and breed landing pages, and ranking that rewards shelters that actually answer adopters.
  • Oscar, the AI adoption assistant: taken from a stranded prototype to a production assistant that finds pets, answers questions and follows up on requests, with deterministic ranking, version-stamped prompts and an eval gate.
  • A contact and payments flow that doesn’t lose people: I fixed a bug that silently dropped free requests, rebuilt donations into a single payment step, and built the paid Express Contact checkout server-side.
  • Welcome Home: a post-adoption essentials catalogue and insurance partnerships, with safety rules enforced by tests.
  • Tools for shelters: a redesigned dashboard, response badges, and reminder emails in each shelter’s own language.
  • Reliability, performance and SEO: I led the response to a database-stall incident, took search from seconds to sub-second, moved pages and images onto the edge cache, and cut 220K redundant URLs from the sitemaps.

How the Platform Fits Together

The platform, top to bottom · click a box
People
Edge
Apps
Services
Data & partners
Web appNext.js 15 · React 19
Server-rendered public site and adopter app; reads the catalogue directly and calls services for writes. Hosts Oscar's chat API.
My work264 merged PRs: search, payments, Oscar, SEO, i18n, security hardening, the first test suite and CI gate.

The public site is a Next.js app behind Cloudflare. It reads the catalogue straight from MongoDB and writes through a small set of Node services for data, accounts and uploads. Scheduled jobs on ECS import listings nightly, pay out shelters, and send reminder and lifecycle emails.

Selected Projects

Search that finds pets nearby, from shelters that reply

I rebuilt the animal search, the shelter directory and the public shelter pages. A radius bar now sits over the map, filters apply as you change them, breed search is scoped to the species, and the results widen their radius instead of running out. Ranking used to give a flat boost to every shelter that had activated its account. When I measured it, activation alone predicted nothing: many activated shelters had never replied to a single request, yet got the same boost as the most responsive ones. I replaced the boost with a ladder tied to measured reply rates. In simulation, the median distance of Atlanta’s first page of results halved, from 14.9 km to 7.4 km, because the lift stopped going to shelters that never answer.

Oscar, the AI adoption assistant

Oscar began as a prototype that had drifted hundreds of commits behind main. I ported it onto the current codebase and rebuilt it on the Vercel AI SDK, with tools created per request so they only ever see that visitor’s context. Then I built the product around it: a docked panel, a full-screen results board, awareness of the pet page you’re on, a proactive “peek” card after engaged browsing, a companion that tracks the status of your requests after you contact a shelter, and food recommendations matched to the life stage of a signed-in visitor’s own pets.

One Oscar turn
BrowserServerLLM
1
A visitor asksBrowser
From the docked panel, the full-screen results board, or a peek card that appears after engaged browsing on a pet page.
2
Rollout gateServer
A config document, read server-side, decides who gets Oscar and holds the kill switch, so launching or rolling back needs no deploy.
3
Assemble the promptServer
Built fresh each turn from the visitor's language, the pet page they are on and, when signed in, their own pets. Every trace carries the prompt version.
4
Tool loop · repeats until the answer is ready
Model streams and calls toolsLLM
Typed tools: search pets, explain how contact works, look up my requests, suggest products for the pet's life stage.
Deterministic engine answersServer
Search and ranking run in TypeScript, not in the model. Cursors and the set of pets shown are signed, so a reply can't invent a pet or a page.
Redact before the model sees itServer
Shelter phone numbers are stripped from listing text. Validated on 14,000+ real listings: zero dialable numbers left, microchip numbers untouched.
5
Pet cards, not paragraphsBrowser
Results render as real pet cards and a results board the visitor can act on, with the reply narrated around them.
Evals gate every prompt change
Scenario suite with an LLM judge; a prompt or model change can't merge without it.
Traced end to end
Every turn traced with its prompt version, through the existing error-monitoring pipeline.
The model narrates. It never computes.

The most important catch came before launch: shelter phone numbers buried in listing descriptions could have reached the model, and Oscar could have repeated numbers that belong behind each shelter’s own contact flow. I wrote a dedicated redactor and validated it on 14,000+ real listings. Zero dialable numbers survived, and microchip numbers, which look a lot like phone numbers, were left untouched.

Early on, healthy instances were being restarted under load. The health check opened a brand-new database connection, with a full TLS handshake, on every probe, which took about 3 seconds against a 5-second timeout. I made it a pure liveness probe that answers in 2–17 ms.

Later, when database load spiked, I led the response. The fixes: one database client with a bounded pool and timeouts, pages that stopped calling their own API over HTTP, single-flight cache fills, bounded third-party calls, a shared Redis cache, and search reads moved to secondaries with hinted indexes. After that, pet pages and images moved onto the Cloudflare edge cache, with a purge whenever a pet is adopted or edited.

Before and after
Each bar is the new value as a share of the old one. Shorter is better.
Speed under load
Search results, no location8–12 s → 0.7 s−93%
Index-served queries read from secondaries instead of an in-memory sort on the primary
Documents read per search60,251 → 5,285−91%
New compound indexes, pinned with hints so the planner can't regress
Radius count query247 ms → 48 ms−81%
Counts inside a radius moved onto a 2dsphere index (Philadelphia, 80 km)
Health check response~3,000 ms → 2–17 ms−99%
A pure liveness probe, so a slow database could no longer get healthy instances restarted
Weight and crawl hygiene
Sitemap URLs submitted269,756 → 48,430−82%
One URL per listing instead of four untranslated copies, adopted pets dropped
Blog header images141.2 MB → 15.2 MB−89%
Camera originals resized and recompressed; worst image 5.6 MB to 124 KB
Measured before and after each change, on production data or a production build.

The edge cache now serves 83% of image requests, and bytes served from the origin fell by 63%.

Contact and payments

Contacting a shelter is the core conversion, and it had a silent leak: a client-side state race let some free requests take the paid branch, where they never reached the shelter. A ref fixed it.

I rebuilt the donation window so a one-time gift and monthly support confirm in a single Payment Element step, with the one-time amount carried into the subscription’s first invoice. I also restored Cash App Pay and fixed two Stripe accounts loading on the same page. Express Contact, the paid tier where Paaw follows up with the shelter for you, now runs its checkout end to end on the server. The payment success page, shared by donations and Express Contact, now handles every state: processing, retry, a declined card, and auto-continue. A deliverability check confirms a shelter can receive messages before an adopter pays to reach it.

Welcome Home and partners

After a request is sent, adopters land on Essentials for pets: a curated partner catalogue with prices refreshed daily, budget filters and search. Safety rules live in tests, not in someone’s memory. For example, products that are toxic to cats can never appear on a cat. Insurance partners are swapped behind one config with a geographic gate, and every outbound click goes through a ledger so commissions can be reconciled.

SEO at scale

The sitemaps were submitting four untranslated copies of every listing, plus thousands of pages for pets that had already been adopted. I cut the submitted set from 269,756 URLs to 48,430 (−82%), gave English a single canonical URL, and fixed the six bugs behind failed Search Console validations. I also expanded the city and breed landing pages across US metros, and added structured About and FAQ entities and an llms.txt for AI search.

Foundations

When I arrived, the web app had no tests and no CI gate. I added the first suite and the gate, and the app now has 340+ test files plus an LLM eval suite for Oscar. I also set up an isolated staging environment with Stripe in test mode, a way to replay the post-contact flow without emailing a real shelter, and security hardening across the web app and the backend services.