Skip to content
← Back to work

Pawbook

A management platform for pet hotels: one system, two audiences, and a security model that never trusts the client.

Role
Design, architecture & full build
Year
2025
Stack
Next.js 16 · TypeScript · Prisma · PostgreSQL · NextAuth v5 · Resend
Live
Visit site
Pawbook business dashboard showing client, dog and request counts with quick actions

Context

One system, two very different users.

Pet hotels and dog daycares run on spreadsheets, WhatsApp threads and paper check-in sheets. Pawbook replaces all of that with one platform that serves two very different users at once: the business owner who needs to run day-to-day operations, and the pet owner who just wants to book a stay and know their dog is fine. The hard part isn't the CRUD. It's keeping every hotel's data provably separate while sharing one codebase and one database.

My role: I designed and built the whole thing end-to-end: data model, auth, both interfaces, the transactional email flow and the deployment.

Architecture

Multi-tenant from the first line.

A single Next.js app backs both surfaces. A Prisma schema over PostgreSQL models businesses, owners, dogs, stays and notifications. Auth is NextAuth v5, and all writes go through server actions rather than sprawling API routes: seven focused modules. New customers onboard through a tokened email activation flow powered by Resend and React Email.

Business dashboard Customer portal Server actions session-scoped · 7 modules PostgreSQL via Prisma businessId derived from the session, never from the request
Both surfaces share one session layer. Every write derives its tenant from the session, and the browser never gets to name a businessId.

Key decisions

Where the senior work lives.

01

The client never picks the tenant.

Every server action derives the businessId from the authenticated session, and deliberately ignores any businessId sent by the browser. It's the single rule that makes tenant isolation hold: a request literally cannot ask for another hotel's data, because it doesn't get to name it. I ran a security pass specifically to close the gaps where that rule could leak.
02

One codebase, two audiences.

The business dashboard and the customer portal are different products with different mental models, but they share a schema and a session layer. Roles and status enums drive what each side can see and do, so the same booking is a task to approve on one side and a request being reviewed on the other.
03

Onboarding that can't deadlock.

Self-service sign-up is where multi-tenant apps quietly break: a user half-registered, no business attached, stuck forever. I built activation around a signed email token and mapped the failure states explicitly, after a round of UAT surfaced exactly that "new user deadlock".
04

Server actions over API sprawl.

Grouping every mutation into seven server-action modules keeps the security-critical code in one place instead of scattered across route handlers. Fewer entry points, fewer things to audit.

Results

2

user surfaces, one codebase

7

server-action modules, all session-scoped

100%

tenant isolation enforced server-side

Live

try it with the demo login

Demo: owner@demo / customer@demo · password demo123

Visuals

Pawbook stays and booking calendar view
The stays calendar, the operational heart of the business dashboard.
Pawbook customer portal where a pet owner manages their dogs and stay requests
The customer portal: the same data, a completely different surface.

Next case

Menu Marvel

An AI companion that reads foreign menus and shows you the dish.