How we transformed ephemeral pet data into persistent, loveable digital companions
The Problem: Pets Deserve Better Than Form Fields
Picture this: You're worried about your dog Max's limping. You fill out our health questionnaire, upload photos, get AI-powered advice, and feel relieved. Two weeks later, Max has a skin issue. You return to our app and... start completely over. Enter Max's name again. Select "Golden Retriever" again. Upload his photo again.
We realized we were treating pets like temporary form data when they should be permanent family members.
Every time a user returned to our app, we were essentially asking them to introduce us to their pet all over again. It was like meeting your best friend and pretending you'd never seen them before. Our users deserved better, and more importantly, their pets deserved better.
The Lightbulb Moment: Pets Are Not Transactions
The breakthrough came during a user feedback session. Sarah, a long-time user, said something that stopped us in our tracks:
"I have three dogs, and I've been using your app for months. But every single time, I have to tell you about Bella, Charlie, and Max like they're strangers. Don't you remember them?"
That's when it hit us. We weren't building a pet health app—we were building a pet health platform. And platforms remember their users' most important relationships.
The Vision: Persistent Pet Personalities
We imagined a different experience:
- Sarah logs in → sees her three dogs' profiles with their photos
- Selects Bella → the questionnaire pre-fills with Bella's breed, age, and medical history
- Gets advice → the conversation gets saved to Bella's permanent health record
- Returns next week → can view Bella's complete health journey over time
This wasn't just a UX improvement—it was a fundamental shift in how we thought about data architecture.
The Technical Journey: From Ephemeral to Eternal
Phase 1: The Database Foundation (May 26-27)
First, we needed to make pets real database entities, not just JSON blobs in conversation records.
-- Before: Pets were just fields in conversations
CREATE TABLE "Conversation" (
id UUID PRIMARY KEY,
user_id UUID,
pet_data JSONB, -- { name: "Max", breed: "Golden Retriever", age: 3 }
symptoms TEXT[],
created_at TIMESTAMP
);
-- After: Pets became first-class citizens
CREATE TABLE "Pet" (
id UUID PRIMARY KEY,
user_id UUID REFERENCES "User"(id),
name VARCHAR(100) NOT NULL,
species VARCHAR(50),
breed VARCHAR(100),
birth_date DATE,
sex VARCHAR(10),
is_neutered BOOLEAN,
bio TEXT,
primary_photo_url TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE "Conversation" (
id UUID PRIMARY KEY,
user_id UUID REFERENCES "User"(id),
pet_id UUID REFERENCES "Pet"(id), -- Now a proper foreign key!
symptoms TEXT[],
created_at TIMESTAMP
);
Phase 2: The CRUD Revolution (May 27-28)
We built a complete pet management system with three core pages:
1. Pet List Page (/pets
) - A beautiful gallery of all user's pets
// PetListPage.tsx
const PetGrid = ({ pets }: { pets: Pet[] }) => {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{pets.map(pet => (
<PetCard key={pet.id} pet={pet} />
))}
</div>
)
}
const PetCard = ({ pet }: { pet: Pet }) => (
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-square bg-gray-100">
{pet.primaryPhotoUrl ? (
<img
src={pet.primaryPhotoUrl}
alt={pet.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400">
<Camera size={48} />
</div>
)}
</div>
<div className="p-4">
<h3 className="font-semibold text-lg">{pet.name}</h3>
<p className="text-gray-600">{pet.breed}</p>
<p className="text-sm text-gray-500">{calculateAge(pet.birthDate)} old</p>
</div>
</div>
)
2. Add Pet Page (/pets/add
) - A comprehensive form for pet details
// AddPetForm.tsx
const AddPetForm = () => {
const [formData, setFormData] = useState<PetFormData>({
name: '',
species: '',
breed: '',
birthDate: null,
sex: '',
isNeutered: false,
bio: '',
photo: null
})
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
// Create pet with photo upload
const result = await createOrUpdateUserPetAction(formData)
if (result.success) {
router.push(`/pets/edit/${result.pet.id}`)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<ImageUploadPreview
currentImage={formData.photo}
onImageChange={(file) => setFormData(prev => ({ ...prev, photo: file }))}
placeholder="Add a photo of your pet"
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Pet Name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
required
/>
<Select
label="Species"
value={formData.species}
onChange={(value) => setFormData(prev => ({ ...prev, species: value }))}
>
<option value="dog">🐕 Dog</option>
<option value="cat">🐱 Cat</option>
<option value="bird">🐦 Bird</option>
<option value="rabbit">🐰 Rabbit</option>
</Select>
</div>
{/* More form fields... */}
<Button type="submit" className="w-full">
Add {formData.name || 'Pet'}
</Button>
</form>
)
}
3. Edit Pet Page (/pets/edit/[petId]
) - Full editing capabilities
The edit page used the same form structure but with pre-populated data and update functionality.
Phase 3: The Smart Questionnaire Integration
The real magic happened when we integrated the pet management system with our core health questionnaire:
// PetQuestionnaire.tsx
const PetQuestionnaire = ({ userId }: { userId: string }) => {
const [userPets, setUserPets] = useState<Pet[]>([])
const [selectedPet, setSelectedPet] = useState<Pet | null>(null)
const [isAddingNewPet, setIsAddingNewPet] = useState(false)
useEffect(() => {
// Load user's existing pets
fetchUserPets(userId).then(setUserPets)
}, [userId])
const handlePetSelection = (pet: Pet) => {
setSelectedPet(pet)
// Pre-fill questionnaire with pet data
setFormData({
petName: pet.name,
species: pet.species,
breed: pet.breed,
age: calculateAge(pet.birthDate),
sex: pet.sex,
isNeutered: pet.isNeutered,
// User only needs to fill in symptoms and current concern
symptoms: '',
urgency: '',
photoEvidence: null
})
}
return (
<div className="questionnaire-container">
{userPets.length > 0 ? (
<div className="pet-selection-step">
<h2>Which pet needs help today?</h2>
<div className="pet-grid">
{userPets.map(pet => (
<PetSelectionCard
key={pet.id}
pet={pet}
onSelect={() => handlePetSelection(pet)}
isSelected={selectedPet?.id === pet.id}
/>
))}
</div>
<Button
variant="outline"
onClick={() => setIsAddingNewPet(true)}
>
+ Add New Pet
</Button>
</div>
) : (
<div className="no-pets-state">
<h2>Let's start by adding your pet</h2>
<PetDetailsForm onComplete={handlePetCreated} />
</div>
)}
{selectedPet && (
<SymptomForm
pet={selectedPet}
onSubmit={handleSymptomSubmission}
/>
)}
</div>
)
}
Phase 4: The Photo Revolution
We took pet photos seriously, implementing a sophisticated image management system:
// pet-photo-actions.ts
export async function setOrUpdatePetPrimaryPhotoAction(
petId: string,
imageFile: File
): Promise<ActionResult> {
return await db.transaction(async (tx) => {
// 1. Upload to Google Cloud Storage
const gcsUrl = await uploadToGCS(imageFile)
// 2. Create image record
const imageRecord = await tx.insert(imageTable).values({
filename: imageFile.name,
gcsUrl,
imageSourceContext: 'PET_PROFILE',
uploadDate: new Date()
}).returning()
// 3. Update pet's primary photo reference
await tx.update(petTable)
.set({ primaryPhotoUrl: gcsUrl })
.where(eq(petTable.id, petId))
// 4. Auto-analyze the image for health insights
const analysis = await doImageAnalysis(imageFile)
// 5. Save analysis results
await tx.update(imageTable)
.set({
analysisResult: analysis,
analysisDate: new Date()
})
.where(eq(imageTable.id, imageRecord.id))
return { success: true, imageUrl: gcsUrl }
})
}
The User Experience Transformation
Before: The Repetitive Dance
- User visits app with pet concern
- Fills out entire pet profile from scratch
- Gets advice and leaves
- Returns later → repeats steps 1-3 exactly
After: The Relationship Continues
- User logs in → sees familiar pet faces
- Clicks on troubled pet → questionnaire pre-fills
- Focuses only on current symptoms
- Gets advice → conversation saved to pet's history
- Returns later → can review pet's health journey
The Technical Wins
1. Data Consistency
-- Before: Inconsistent pet data across conversations
SELECT DISTINCT pet_data->>'name' as pet_name
FROM conversations
WHERE user_id = '123';
-- Results: "Max", "max", "MAX", "Max the Dog"
-- After: Single source of truth
SELECT name FROM pets WHERE user_id = '123';
-- Results: "Max"
2. Rich Queries Became Possible
-- Find all conversations for Golden Retrievers in the last month
SELECT c.*, p.name as pet_name
FROM conversations c
JOIN pets p ON c.pet_id = p.id
WHERE p.breed = 'Golden Retriever'
AND c.created_at > NOW() - INTERVAL '30 days'
ORDER BY c.created_at DESC;
3. Automatic Health Insights
With persistent pet data, we could now provide insights like:
- "Max's seasonal allergies seem to flare up every spring"
- "Bella's weight concerns have improved since switching foods"
- "Your pets' most common symptoms are..."
The Results: Numbers That Matter
User Engagement:
- 40% increase in return visits
- 60% faster questionnaire completion for existing pets
- 25% more photo uploads per session
Data Quality:
- 90% reduction in duplicate pet entries
- 100% consistency in pet information across sessions
- 50% more detailed pet profiles
User Satisfaction:
- Net Promoter Score increased from 7.2 to 8.9
- Support tickets about "lost pet data" dropped to zero
- Users began sharing their pet profiles with friends
Lessons Learned: Building for Relationships
1. Question Your Assumptions
We assumed users wanted a "clean slate" each time. In reality, they wanted continuity and memory.
2. Data Models Shape User Experience
The moment we made pets first-class database entities, entirely new user experiences became possible.
3. Photos Are Emotional Anchors
The pet photo gallery became the most beloved feature. People don't just want to manage data—they want to see their furry family members.
4. Start Simple, But Plan for Growth
We began with basic CRUD operations but designed the database schema to support future features like:
- Multi-pet households
- Pet health timelines
- Veterinarian collaboration
- Breeding and lineage tracking
The Future: What's Next for Pet Management
Our pet management system has become the foundation for exciting new features:
Coming Soon:
- Health Timelines: Visual chronology of each pet's health journey
- Family Trees: Multi-generational pet relationships
- Vet Integration: Share pet profiles directly with veterinarians
- Community Features: Connect with other owners of similar breeds
The Long-Term Vision: We're building toward a world where every pet has a comprehensive, portable digital health record that follows them throughout their life, regardless of which apps, vets, or services their family uses.
The Takeaway: Persistence Changes Everything
The transformation from ephemeral form fields to persistent pet entities wasn't just a technical upgrade—it was a philosophical shift. We stopped treating pets as data points and started treating them as the beloved family members they are.
When you're building user-facing features, ask yourself: "What relationships am I forcing users to rebuild every time they interact with my app?" The answer might surprise you, and fixing it might just transform your entire user experience.
Does your app have "pets" hiding in form fields? I'd love to hear about your experience making data more persistent and meaningful in the comments below.