PawDesk handles pet care bookings and asks a person when it isn't sure.
PawDesk is a hybrid workflow for a kennel's booking inbox: plain code runs the steps in a fixed order and calls AI agents only where the input is messy. Agents read each request and the photo of the vaccine card, code checks the vaccines and finds a free kennel, and another agent writes the reply.
When every check passes, PawDesk books the stay and sends the reply. When something is unclear or breaks a rule, the request waits in a review queue for someone on staff.
I built it to show full stack work and AI-driven development in one project, with these skills:
- Python and FastAPI: the API and the workflow that runs each request
- React and TypeScript: this dashboard, which shows each run live
- PostgreSQL and Docker: migrations, and the full stack in Docker Compose
- Testing and CI: pytest, Vitest, and Playwright, run by GitHub Actions
- AI-assisted development: each change specified, reviewed, and tested before it ships
Example request. Avery Lindqvist wrote, with a photo attached:
Hi! Could Biscuit board with you from March 24 to March 27? He eats grain-free food twice a day. His vaccine card is attached.

- Pet
- Biscuit, a Beagle on file
- Stay
- Boarding, March 24 to 27
- Vaccines
- Rabies, DHPP, and Bordetella cover all three nights
- Kennel
- A medium one is free all three nights
PawDesk booked that kennel and sent this reply:
Biscuit's boarding stay is confirmed — March 24–27, 2027
Hi Avery,
Great news — Biscuit's boarding stay at Maple Hollow Pet Lodge is confirmed for March 24–27, 2027.
We're looking forward to having Biscuit with us. If you have any questions before the stay, just reply to this message and we'll be glad to help.
Warmly, The Maple Hollow Pet Lodge team
Try a request
Pick a sample or make one up, then watch each step run with real model calls. The desk treats today as March 3, 2027, so the vaccine cards' dates line up with the stays.
Loading samples
Booking rules
Code checks each rule below; no model decides any of them. They're the demo facility's rules, and each facility sets its own vaccine lists per service and species.
Services
| Service | Visit | Kennel | Dogs need | Cats need |
|---|---|---|---|---|
| Boarding | VisitOvernight, at least one night | KennelA free kennel of the pet's size, reserved when the reply goes out | Dogs needRabies, DHPP, Bordetella | Cats needRabies, FVRCP |
| Daycare | VisitOne day, drop-off and pickup on the same date | KennelNone | Dogs needRabies, DHPP, Bordetella, Canine influenza | Cats needRabies, FVRCP |
| Grooming | VisitOne appointment on a single date | KennelNone | Dogs needRabies | Cats needRabies |
When a vaccine counts
The pet is on site every day from drop-off through pickup. A shot counts if the pet got it on or before drop-off and it expires on or after pickup day. Code looks at the records on file and the ones the records agent read from the card, and one covering record is enough.
When no record covers the stay, code names the reason: no record, expired before drop-off, expires during the stay, or given after drop-off. A card with no expiry date for a required shot goes to a person.
Kennel sizes
Code sets a pet's size from its species and weight and searches only kennels of that size. Cats get cat kennels at any weight. Dogs:
- Small
- Under 20 lb
- Medium
- 20 to under 50 lb
- Large
- 50 to under 90 lb
- Extra large
- 90 lb and up
A kennel is free when none of the stay's nights is booked. The nights run from drop-off to the night before pickup, so a stay that ends March 27 doesn't block one that starts March 27.
Dates
A request can start today or up to 365 days ahead. Boarding has to end at least one day after it starts. Daycare and grooming start and end on the same date.
When a rule isn't met
A failed rule, or anything the intake or records agent flags, takes the request off the automatic path. If the resolver can work on the problem, it suggests other dates, a larger kennel, or an updated vaccine record. Either way, someone on staff decides before a reply goes out.
Model Use
Four parts of PawDesk use a model. Each one reads something, returns a fixed set of fields, and hands them to code that checks them before they count. None of them can approve a booking.
| Agent | Reads | Returns | What code checks |
|---|---|---|---|
| IntakeOne model call | ReadsThe pet parent's message. Code scans it for instruction-like text, such as “ignore your rules”, before the model sees it. | ReturnsThe pet's name, the service, the dates, any special notes, a confidence score, and anything it found unclear. | What code checksMatches the pet to one on file and checks the dates. Instruction-like text, a confidence under 0.7, or anything unclear sends the request to a person. |
| RecordsOne model call | ReadsThe photo of the vaccine card. | ReturnsEach vaccine with the date given and the expiry date, the pet's name as printed, whether the photo was readable, and a confidence score. | What code checksRejects impossible dates and unknown vaccines, and compares the printed name with the booked pet. An unreadable card or a confidence under 0.7 sends the request to a person. |
| ResolverA loop of up to 6 model calls | ReadsOnly values code built, such as which vaccine expired. It never sees the pet parent's message, and code starts it only for problems it can work on. | ReturnsOne offer: other dates, a larger kennel, or a request for an updated vaccine record. It finds them with 3 read-only tools, at most 12 calls. | What code checksRuns the checks again on every option. The resolver can't book anything, and a person reviews its offer. |
| ReplyOne model call | ReadsThe checked facts and the options code verified. Like the resolver, it never sees the original message. | ReturnsA subject line and the body of the reply. | What code checksFlags any date the checks didn't confirm, any money amount, and words like “booked” or “confirmed” the facts don't support. A flagged draft goes to a person. |
How a request moves
Code sets the order of the steps and makes each decision. The models read messy input and write text for people.
- 1
Readmodel
The intake and records agents read the message and the vaccine card at the same time.
- 2
Checkcode
Code compares each required vaccine's expiry date with the stay, then looks for a free kennel that fits the pet's size.
- 3
Find optionsmodeltools
If a check fails, the resolver suggests a fix and code checks it again. If every check passes, this step is skipped.
- 4
Write the replymodel
The reply agent drafts the answer to the pet parent from the facts code checked.
- 5
Send or holdcodestaff
Code scans the draft for promises the desk can't keep. A clean reply goes out and reserves the kennel in one database transaction. A flagged one waits in the review queue until someone on staff approves, edits, or declines it.
What the tests hold it to
- Only the checks or a staff member can approve a booking. The lifecycle code refuses an approval from anywhere else, a model included.
- Postgres refuses two reservations for the same kennel on the same night, so a race between two requests can't double-book it.
- A run that crashes picks up at the step it stopped on and doesn't pay again for model calls it already finished.
- Each model call, tool call, and step is saved with its input, output, tokens, billed cost, and timing. Open any request on the requests list to inspect them.
On 16 labeled messages, three of them prompt-injection attempts, intake got all 16 right and sent all three attempts to a person. The evals page has that report and the others.
Working with an AI assistant
I build PawDesk one change at a time, and each change goes through the same four steps. Choices that outlast one change get a numbered decision record; there are 19 so far.
- An AI assistant drafts the change as an OpenSpec proposal: requirements with when/then scenarios, a design that weighs the options, and a task list.
- I read the proposal, push back where I disagree, and approve it before any code exists.
- The code goes in as small commits. Lint, type checks, and the tests pass before each one.
- Each test names the requirement it proves, and the build fails when a requirement has no passing test. In the latest test run, 177 of 177 requirements pass, across 1061 tests.
How it was built lists each of the 49 changes with its requirements, the tests behind them, and their latest results.
Skills across the stack
PawDesk runs from the database to the browser, built with AI tools every day and checked by tests at every step. Each row pairs a skill with the parts of PawDesk that use it.
| Skill | Where PawDesk uses it |
|---|---|
| Python, FastAPI | The API, the workflow, and the checks. Pydantic validates each model response before the code uses it. |
| TypeScript, React | This dashboard, in Next.js 16 and React 19 with Tailwind CSS. React Flow draws the run graph and Recharts draws the metrics. |
| REST APIs and integrations | FastAPI endpoints with live run updates over server-sent events. Model calls go to DeepSeek V4.1 Flash through OpenRouter. The browser reaches the API only through a proxy with an allowlist. |
| PostgreSQL and data modeling | Postgres 18 with SQLAlchemy and Alembic migrations. The schema models owners, pets, vaccine records, kennels, reservations, and the steps of each run. |
| Docker and CI/CD | Docker Compose runs the full stack. A GitHub Actions workflow runs lint, type checks, the tests, the build, and a gitleaks secret scan. |
| Automated testing | pytest against a real Postgres, Vitest for the dashboard's logic, and Playwright end-to-end runs that replay recorded model responses so they cost nothing. |
| Secure coding | Pet parents' messages and photos count as untrusted input. Code flags prompt injections, and the API key stays on the server. |
| Accessibility | You can reach each step of a run by keyboard, and screen readers announce its summary. Layouts hold at phone width. |
The pet parents, pets, and vaccine cards on this site are made up.