All work Case study · Rocha Tech LLC

Mo Paws — a multi-model AI pet companion

A cross-platform assistant for dog owners: breed-aware answers, a structured behaviour assessment, and a health schedule that actually remembers what's due. Owned end to end — auth and data model through to the routing layer that decides which model answers each question.

RoleSoftware Engineer — full stack
TimelineMay 2026 → ongoing
PlatformWeb + iOS / Android
StatusClosed TestFlight beta
01

At a glance

206breed profiles — 205/205 of the stored AKC baseline, alongside 181 canine knowledge facts
−75–80%cold-start latency on first response after optimisation
3model providers behind one router, with automatic fallback
1codebase shipping to web, iOS and Android via Capacitor
02

The problem

Ask a general-purpose chatbot "how much should my 4-month-old Pomeranian be eating?" and you get a confident, generic answer that ignores breed, age and the fact that you asked the same thing last week. Ask a forum and you get eleven contradictory answers. Neither remembers your dog.

Mo Paws is the version I wanted for Mocha, my Pomeranian: an assistant that knows the breed, keeps the pet's profile and health schedule as first-class data, and gives an answer grounded in a curated source rather than whatever the model happened to memorise.

The engineering problem underneath it is unglamorous and specific — make it fast, make it cheap, and make it impossible for one user to read another user's pet data.

03

Constraints I designed against

  • Consumer latency budget. A pet owner on a phone abandons a screen that thinks for four seconds. First token has to feel immediate.
  • A hard cost ceiling. Small team, no enterprise budget — per-request cost is a design constraint, not an afterthought.
  • Multi-tenant from day one. Every row belongs to a user. A bug in an API handler must not be able to leak another household's pet.
  • Correctness where it matters. Feeding amounts, vaccination intervals and breed-specific risk are not places to let a model improvise.
  • One person, one codebase. Anything that doubles the maintenance surface has to earn it.
04

Architecture

Client Vite · React · TS
Capacitor shell → Chat (SSE stream) Behaviour assessment Health schedule
Request path edge function
Supabase Auth (JWT) → Model router → Schema validation + retry
Retrieval deterministic
Breed resolver → 206 breed profiles → Pet records · reminders · logs → Evidence set
Models cost / latency aware
DeepSeek GPT-4.1-mini Claude Haiku ↻ provider fallback
Response schema-shaped
Structured sections → Cited evidence → Action previews → Disclaimer
Data Postgres
Row Level Security pets · profiles · schedules versioned migrations

Highlighted nodes are the three places most of the engineering time went.

05

In the product

06

Decisions & trade-offs

Rule-based retrieval instead of a vector index

Decision
Resolve breed and topic deterministically, then pull the matching entries from a curated knowledge base — no embeddings, no vector store.
Why
At a few hundred well-structured entries, exact matching beats similarity search on both latency and correctness. "Pomeranian" should never retrieve "Pomeranian-adjacent."
Trade-off
It does not generalise to free-form questions outside the taxonomy. Past a few thousand entries this has to become hybrid retrieval — and that migration is designed for, not deferred blindly.

One router, three providers

Decision
Route each request to a model by class of work — cheap and fast for short factual turns, stronger models for assessment synthesis — with automatic fallback when a provider errors or times out.
Why
Cost and latency are per-request properties, not per-app ones. Fallback also means one provider's outage is a slower answer instead of a broken app.
Trade-off
Three prompt surfaces to keep behaviourally consistent. Every prompt change gets a regression pass across providers before it ships.

RLS as the authorization boundary

Decision
Ownership rules live in Postgres Row Level Security policies, not in application middleware. The API layer cannot query rows it shouldn't see, even if a handler forgets a filter.
Why
Application-layer authorization fails open when someone adds an endpoint in a hurry. Database-layer authorization fails closed.
Trade-off
Policies are harder to debug than an if statement, and every schema migration has to carry its policy changes with it.

Streaming + preloading over a faster model

Decision
Stream tokens over SSE and preload the knowledge base and session context ahead of the first question, rather than chasing raw model speed.
Why
Perceived latency is dominated by time-to-first-token and cold-start work, not total generation time. This is where the 75–80% cold-start reduction came from.
Trade-off
More client-side state to keep coherent, and streaming makes structured-output validation harder — the schema check happens on the completed message, with a retry path when it fails.

Answers cite what they read

Decision
Every response carries the set of records it consulted — profile, reminders, care timeline, recent logs — and shows the owner how many sources it used.
Why
The difference between an assistant and a fluent guesser is whether a user can check where the answer came from. It also makes a bad answer debuggable instead of mysterious.
Trade-off
The retrieved set has to be threaded through the entire prompt-assembly path, and an answer that consulted nothing has to say so rather than sounding equally confident.

The model proposes; the owner commits

Decision
Suggested next steps render as previews. Nothing is written to the pet's schedule or records until the owner taps and saves it.
Why
A model that can silently mutate someone's care schedule turns every bad suggestion into data cleanup. Keeping writes behind an explicit tap keeps the assistant advisory.
Trade-off
More taps, and a genuinely good suggestion still needs confirming. Worth it for anything touching health data.

Capacitor instead of a native rewrite

Decision
Ship the React app into iOS and Android through Capacitor rather than maintaining native clients.
Why
One person cannot maintain three UIs. The app is chat, forms and lists — none of it needs native rendering.
Trade-off
Native polish (gesture feel, deep OS integration) is capped. Acceptable now; the first feature that truly needs native is the trigger to revisit.

Assessment grounded in published frameworks

Decision
Build the dog behaviour assessment on the C-BARQ and Dognition frameworks rather than inventing a questionnaire.
Why
Established instruments give the output a defensible basis, and the scoring is deterministic — the model interprets results, it doesn't produce them.
Trade-off
Longer questionnaires than a "fun quiz," which costs completion rate. Mitigated by chunking it into short, resumable sections.
07

Where it landed

  • Cold-start latency down 75–80%. Preloading plus routing moved first response from "is this broken?" to conversational.
  • Full breed coverage. 206 breed profiles spanning 205/205 of the stored AKC baseline list, so breed lookups never fall back to model memory.
  • No cross-tenant data path. Every pet, profile and schedule row is gated by RLS policy rather than by handler discipline.
  • Single codebase across three platforms, with the same auth, retrieval and routing behaviour on each.
  • Answers are attributable. Each response lists the records it read and how many sources it used, so a wrong answer can be traced instead of argued about.
  • No silent writes. Every AI-proposed care item needs an explicit confirmation before it touches the pet's schedule.
  • Provider-outage tolerance. A failing provider degrades to a fallback model instead of an error screen.
08

What I'd do differently

Build the eval harness before the third prompt rewrite. Today prompt changes get a regression pass and structured-output schema validation with retries — which catches format breakage but not quality drift. A proper offline eval set with graded answers should have existed from the start; retrofitting one means reconstructing the cases I already fixed by hand.

Instrument cost per conversation earlier. I added latency and cost awareness to the router before I had good per-conversation attribution, so the first version of the routing policy was reasoned from first principles instead of measured. Measurement changed two of the thresholds.

Treat the knowledge base as a versioned artifact sooner. It started as content and became a dependency. It should have had a schema, a validator and a review step from entry one.

Copied