Skip to main content
RapidDev - Software Development Agency
App Featuresvertical-tools24 min read

How to Add a Pet Adoption Finder to Your App (Copy-Paste Prompts Included)

A pet adoption finder needs species/breed/location filters, pet profile cards with photo galleries, a map view showing shelter locations, and a favorites and inquiry system. With Lovable or V0 you can ship a working version in 4–8 hours using Supabase for data and Mapbox for maps. Costs stay at $0 until map loads or email volume exceeds free tiers. The Petfinder API requires a server-side proxy — direct browser calls fail with CORS errors.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

vertical-tools

Build with AI

4-8 hours with Lovable or V0

Custom build

1-3 weeks custom dev

Running cost

$0/mo early stage · $75-150/mo at 10K users

Works on

WebMobile

Everything it takes to ship a Pet Adoption Finder — parts, prompts, and real costs.

TL;DR

A pet adoption finder needs species/breed/location filters, pet profile cards with photo galleries, a map view showing shelter locations, and a favorites and inquiry system. With Lovable or V0 you can ship a working version in 4–8 hours using Supabase for data and Mapbox for maps. Costs stay at $0 until map loads or email volume exceeds free tiers. The Petfinder API requires a server-side proxy — direct browser calls fail with CORS errors.

What a Pet Adoption Finder Actually Is

A pet adoption finder is a searchable directory of available animals — typically sourced from local shelter data you manage in Supabase, or pulled live from the Petfinder API covering 14,000+ US shelters. The core experience has three layers: discovery (filter by species, breed, age, size, and location), connection (favorite a pet, submit an inquiry form), and status (real-time adopted/pending/available badge). The product decisions that shape the build are whether data comes from your own database or the Petfinder API, whether you include a map view (adds Mapbox token management), and how adoption inquiries are handled (Supabase + Resend email vs a full application form with document uploads). Most first builds use a self-managed Supabase pets table; Petfinder integration is added later as a data enrichment step.

What users consider table stakes in 2026

  • Filter panel covering species (dog, cat, rabbit, bird, other), breed search, age range, size (small, medium, large), gender, and location radius — all filters combined with AND logic
  • Pet profile cards showing a photo, name, species, breed, age, and a color-coded availability status badge (green: available, yellow: pending, grey: adopted)
  • Map view plotting shelter locations as pins; clicking a pin highlights the pets from that shelter in the grid
  • Favorites heart icon that updates optimistically (instant visual feedback before the DB write confirms)
  • Inquiry or adoption application form that captures name, email, phone, and message tied to the specific pet
  • Real-time availability status — when a pet is adopted, its badge updates without requiring a page refresh
  • Shareable pet profile URL so potential adopters can send a link to a specific animal

Anatomy of the Feature

Six components. AI tools handle the listing grid, filter sidebar, and inquiry form reliably. The Mapbox integration, the Petfinder API proxy, and the dynamic filter query builder are the three places builds break.

Layers:UIDataBackendService

Filter sidebar / filter drawer

UI

On desktop: a fixed left sidebar with shadcn/ui CheckboxGroup for species and breed, an age range Slider, a size multi-select, and a location radius input. On mobile: a shadcn/ui Sheet (bottom drawer) triggered by a Filter button. Filter state is managed in React state or Zustand. Each change fires a new Supabase query — only filters with a selected value are added to the WHERE clause.

Note: The most common AI mistake is including empty filter values in the WHERE clause — e.g., WHERE breed = '' filters out all pets. Build the query conditionally.

Pet listing grid

UI

CSS Grid (web) or Flutter GridView.builder (mobile) rendering one pet card per row. Each card shows the primary photo from Supabase Storage, the pet's name, species, breed, age in human-readable format ('2 years', 'Puppy'), and an availability status badge. Pagination via a 'Load more' button (12 pets per page) or infinite scroll using react-intersection-observer.

Note: Store age_months as an integer in the DB and convert to display string in the UI — this makes age range filtering straightforward with a numeric BETWEEN query.

Map view layer

Service

Mapbox GL JS (web) or flutter_map (mobile) renders shelter location pins from the location_lat and location_lng columns on the pets table (or a shelters table). The Mapbox Geocoding API converts shelter street addresses to coordinates during data entry. Clicking a map pin filters the pet grid to show only pets from that shelter. The Mapbox public token must be stored as an environment variable — never hardcoded in component code.

Note: Mapbox GL JS CSS (mapbox-gl/dist/mapbox-gl.css) must be imported in the root layout in Next.js. Missing the CSS import causes invisible map controls and broken pin interactions.

Pet profile page

UI

Individual pet page at /pets/[id] with a photo carousel (shadcn/ui Carousel or photo_view Flutter package), a details panel showing breed, age, temperament tags, and shelter contact info, and a 'Start Adoption Inquiry' CTA. The page is server-rendered (Next.js) or pre-fetched (Flutter) using the pet ID from the URL.

Note: Add an og:image meta tag pointing to the primary pet photo — shareable URLs on social media will show the pet's photo in the preview card.

Inquiry and application form

Backend

react-hook-form + zod (web) or Flutter Form widget (mobile) capturing adopter name, email, phone, and message alongside the specific pet ID. On submit, writes to a Supabase adoption_inquiries table and triggers a Supabase Edge Function that sends an email notification to the shelter via Resend. The adopter receives a confirmation email with the pet's name and inquiry ID.

Note: Rate-limit inquiry submissions per pet per user to prevent spam — one inquiry per user per pet is typically the right limit, enforced with a unique constraint on (user_id, pet_id) in adoption_inquiries.

Favorites store

Data

Supabase user_favorites table (user_id, pet_id) with RLS restricting reads and writes to auth.uid() = user_id. The heart icon updates optimistically on tap — the UI flips immediately and the DB write happens in the background. If the write fails, the UI reverts. Unauthenticated users are redirected to login before the favorite is recorded.

Note: Use a unique constraint on (user_id, pet_id) to prevent duplicate favorites. The constraint also enables an 'upsert' pattern that toggles: if the row exists, delete it; if not, insert it.

Petfinder API client

Service

Optional live data source: Petfinder API v2 (petfinder.com/developers) covers 14,000+ US shelters via OAuth 2.0 client_credentials flow. Returns pet listings including photos, breed, age, and shelter contact. Must be called from a Supabase Edge Function (Lovable) or Next.js API route (V0) — direct browser calls fail with CORS errors. Token refresh must be handled server-side; rate limit is 1,000 requests/day on the free tier.

Note: Petfinder API pet listings can be synced into your Supabase pets table on a schedule using a Supabase Edge Function to get the best of both worlds: live data with fast DB-backed filtering.

The data model

Four tables covering the full adoption flow. Run this in the Supabase SQL editor — pets and pet_photos are publicly readable; user_favorites and adoption_inquiries require authentication.

schema.sql
1create table public.pets (
2 id uuid primary key default gen_random_uuid(),
3 shelter_id uuid,
4 name text not null,
5 species text not null,
6 breed text,
7 age_months int not null default 0,
8 gender text check (gender in ('male', 'female', 'unknown')),
9 size text check (size in ('small', 'medium', 'large', 'extra-large')),
10 status text not null default 'available' check (status in ('available', 'pending', 'adopted')),
11 description text,
12 location_lat decimal(9,6),
13 location_lng decimal(9,6),
14 temperament_tags jsonb not null default '[]',
15 created_at timestamptz not null default now(),
16 updated_at timestamptz not null default now()
17);
18
19create table public.pet_photos (
20 id uuid primary key default gen_random_uuid(),
21 pet_id uuid references public.pets(id) on delete cascade not null,
22 image_url text not null,
23 sort_order int not null default 0
24);
25
26create table public.user_favorites (
27 id uuid primary key default gen_random_uuid(),
28 user_id uuid references auth.users(id) on delete cascade not null,
29 pet_id uuid references public.pets(id) on delete cascade not null,
30 created_at timestamptz not null default now(),
31 unique (user_id, pet_id)
32);
33
34create table public.adoption_inquiries (
35 id uuid primary key default gen_random_uuid(),
36 user_id uuid references auth.users(id) on delete cascade,
37 pet_id uuid references public.pets(id) on delete cascade not null,
38 adopter_name text not null,
39 adopter_email text not null,
40 adopter_phone text,
41 message text,
42 status text not null default 'pending' check (status in ('pending', 'reviewed', 'approved', 'declined')),
43 created_at timestamptz not null default now(),
44 unique (user_id, pet_id)
45);
46
47-- Enable RLS
48alter table public.pets enable row level security;
49alter table public.pet_photos enable row level security;
50alter table public.user_favorites enable row level security;
51alter table public.adoption_inquiries enable row level security;
52
53-- Public read for pet discovery
54create policy "Public can view pets"
55 on public.pets for select using (true);
56
57create policy "Public can view pet photos"
58 on public.pet_photos for select using (true);
59
60-- Favorites: only the owning user
61create policy "Users can manage own favorites"
62 on public.user_favorites for all
63 using (auth.uid() = user_id)
64 with check (auth.uid() = user_id);
65
66-- Inquiries: users can insert and view their own
67create policy "Users can submit inquiries"
68 on public.adoption_inquiries for insert
69 with check (auth.uid() = user_id);
70
71create policy "Users can view own inquiries"
72 on public.adoption_inquiries for select
73 using (auth.uid() = user_id);
74
75-- Indexes
76create index pets_species_status_idx on public.pets (species, status);
77create index pets_location_idx on public.pets (location_lat, location_lng);
78create index pet_photos_pet_sort_idx on public.pet_photos (pet_id, sort_order);
79create index user_favorites_user_idx on public.user_favorites (user_id);
80create index adoption_inquiries_pet_idx on public.adoption_inquiries (pet_id);

Heads up: The unique constraint on (user_id, pet_id) in user_favorites enables toggle behavior with a single upsert — if the row exists it can be deleted, if not it is inserted. The public SELECT policy on pets and pet_photos is intentional: pet discovery should work without logging in. Favorites and inquiries require authentication.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

Required for integrating with existing shelter management software, syncing adoption status back to Petfinder via their Partner API, building a multi-shelter platform with branded subdomains, or implementing adoption application scoring with document uploads.

Step by step

  1. 1Next.js App Router with Supabase — ISR for pet listing pages to enable Google indexing of individual pet profiles
  2. 2Scheduled Supabase Edge Function that pulls new pets from the Petfinder API every 30 minutes, syncs them to the pets table, and marks adopted pets automatically
  3. 3Multi-shelter admin portal: per-shelter Supabase RLS using a shelter_id scoped policy; each shelter's staff can only edit their own pets
  4. 4Adoption application pipeline: file upload for vet references and landlord letters via Supabase Storage private buckets; status workflow (submitted → under review → approved → declined) with email notifications at each stage

Where this path bites

  • Petfinder Partner API access (for writing adopted status back) requires a separate partnership agreement with Petfinder, not just a developer API key

Third-party services you'll need

Most costs at early scale are zero. Map loads and email notifications are what push costs up at 1K+ active users.

ServiceWhat it doesFree tierPaid from
Petfinder API v2Live shelter pet listings from 14,000+ US shelters via OAuth 2.01,000 requests/dayContact Petfinder for higher limits
Mapbox GL JSInteractive map for shelter proximity view and location pins50,000 map loads/monthFrom $5/1,000 map loads above free tier
SupabaseDatabase (pets, favorites, inquiries), auth, Storage for pet photos500MB DB, 1GB storage, 2 projects$25/mo (Pro)
ResendAdoption inquiry email notifications to shelters and confirmation emails to adopters3,000 emails/month$20/mo for 50,000 emails
CloudinaryOptional pet photo optimization and CDN — serves images at optimized sizes for card grids25GB storageFrom $89/mo (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Petfinder free tier handles data sourcing. Mapbox free tier covers 50,000 map loads. Supabase free tier covers DB and photo storage. Resend free tier covers inquiry emails.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Petfinder API calls fail in Lovable preview with a CORS error

Symptom: The Petfinder API v2 does not set Access-Control-Allow-Origin headers for browser requests. Any call made directly from a React component — whether in Lovable preview or in the deployed app — returns a CORS error and never reaches the API. This is a server-side API that requires the OAuth client_credentials token exchange to happen outside the browser.

Fix: Create a Supabase Edge Function (in Lovable) or a Next.js API route (in V0) that performs the Petfinder OAuth token exchange using client_id and client_secret stored in Secrets/env vars via Deno.env.get(), then forwards the search request with the Bearer token. The client-side code calls your Edge Function, not Petfinder directly.

Map pins don't appear even though lat/long values exist in the database

Symptom: Two separate causes: (1) The Mapbox public token was hardcoded in the component instead of read from an env var, rotated or restricted after the fact, and now returns a 401 that silently fails. (2) The Mapbox GL JS CSS file (mapbox-gl/dist/mapbox-gl.css) was not imported in the root layout, which causes the map container to render with zero height and invisible controls even though the map object initializes correctly.

Fix: Always read the Mapbox token from NEXT_PUBLIC_MAPBOX_TOKEN (V0) or import.meta.env.VITE_MAPBOX_TOKEN (Lovable). In Next.js, add import 'mapbox-gl/dist/mapbox-gl.css' to the root layout.tsx. Verify by inspecting the map container element in DevTools — a missing CSS import results in height: 0 on the container.

Favorites heart resets to unfavorited on page refresh

Symptom: When the favorites heart state is stored only in React local state (useState) without reading from Supabase on mount, the component always initializes to 'unfavorited' on page refresh — even if the user has 10 favorites saved in the DB. This makes the favorites feature feel broken and causes users to add the same pet multiple times, hitting the unique constraint and triggering a 409 error.

Fix: On component mount (or in the page's data fetch), query the user_favorites table for the current user and initialize the local favorites state from the DB result. For the pet grid, batch-fetch the user's favorite pet_ids for the current page in a single query: SELECT pet_id FROM user_favorites WHERE user_id = auth.uid() AND pet_id = ANY(array_of_visible_pet_ids).

Filter combinations return zero results even when matching pets exist

Symptom: When all filter fields are included in the WHERE clause unconditionally, an unselected filter (e.g., breed left empty by the user) becomes WHERE breed = '' or WHERE breed ILIKE '%%', which filters out pets with NULL breed values or returns unexpected results. The symptom is that adding any second filter suddenly returns no results, even when pets clearly match both criteria.

Fix: Build the Supabase query dynamically. Start with a base query: let q = supabase.from('pets').select('*'); then conditionally chain each filter only when its value is non-empty: if (species) q = q.eq('species', species); if (breed) q = q.ilike('breed', '%' + breed + '%'). Never pass empty strings or null values to filter methods.

Best practices

1

Always proxy Petfinder API calls through a Supabase Edge Function or Next.js API route — the API does not support direct browser requests and CORS errors will appear in every environment

2

Build the Supabase filter query dynamically, adding each clause only when the user has selected a value — unconditional empty-string filters silently break multi-filter combinations

3

Use optimistic UI updates for the favorites heart: flip the visual state immediately, then write to Supabase in the background and revert if the write fails — this makes the feature feel instant

4

Import Mapbox GL JS CSS in the root layout, not just in the map component — missing this import causes the map container to collapse to zero height even though the JavaScript initializes correctly

5

Store location_lat and location_lng as decimal(9,6) on the pets table so radius-based queries work correctly with a Haversine formula filter

6

Add a unique constraint on (user_id, pet_id) in user_favorites to prevent duplicate favorites and enable upsert-based toggle behavior

7

Set the availability status badge (available/pending/adopted) prominently on every pet card — users filter heavily by availability and missing this information creates frustrating dead-end clicks

8

Add Supabase Realtime on the pets table so shelter staff can mark a pet as adopted and the status badge updates across all active user sessions without a page refresh

When You Need Custom Development

Lovable and FlutterFlow handle self-managed shelter databases and basic Petfinder API integration well. These scenarios outgrow an AI-built implementation:

  • Shelter needs an admin portal to bulk upload pets from existing shelter management software (PetPoint, Shelter Buddy) — requires an import pipeline that maps each system's field schema to your Supabase tables
  • App must sync adopted status back to Petfinder in real time via the Petfinder Partner API — requires a separate partnership agreement with Petfinder and webhook integration beyond the standard developer API
  • Multi-shelter platform where each shelter has its own branded subdomain, separate admin login, and per-shelter analytics — requires multi-tenant Supabase RLS, subdomain routing in Next.js, and a shelter onboarding flow
  • Adoption application workflow with background check integration, landlord verification document uploads, and a scoring or approval workflow beyond a simple inquiry form

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Can I pull real pet listings from existing shelters without entering them manually?

Yes — the Petfinder API v2 provides access to listings from 14,000+ US shelters. You set up a Supabase Edge Function that authenticates with Petfinder using OAuth client credentials, fetches pets by location and species, and syncs them into your Supabase pets table on a schedule. The UI then queries your Supabase table rather than Petfinder directly, which gives you fast filtering without API rate limit concerns. The Petfinder free tier allows 1,000 API requests per day, which is enough for a sync-every-30-minutes schedule.

How do I show pets available near the user's current location?

Use the browser Geolocation API (navigator.geolocation.getCurrentPosition) to get the user's coordinates, then query Supabase for pets within a radius using the Haversine formula in a raw SQL query. For a simpler approximation, filter by a bounding box: WHERE location_lat BETWEEN (user_lat - radius_deg) AND (user_lat + radius_deg) AND location_lng BETWEEN (user_lng - radius_deg) AND (user_lng + radius_deg). Add a PostGIS extension in Supabase for accurate geospatial queries at scale — ST_DWithin(point, center, radius_meters) is the proper function once you need precision.

What happens when a pet gets adopted — how do I update the status in real time?

Update the status column on the pets row to 'adopted'. With Supabase Realtime enabled on the pets table, any open browser session subscribed to that table receives the change within about one second and the status badge updates from green to grey automatically. For the Petfinder integration path, Petfinder updates their own listing data, and your sync Edge Function picks up the status change on the next scheduled run.

Can users save favorite pets and get notified if their status changes?

Yes on both counts. Favorites are stored in the user_favorites table. For status change notifications, add a Supabase Database Webhook on the pets table that triggers a Supabase Edge Function whenever a pet's status changes. The function queries user_favorites for all users who have favorited that pet and sends them an email via Resend. For push notifications on mobile, route through OneSignal or Firebase Cloud Messaging instead of email.

How do I add breed-specific information and temperament tags for each pet?

Store temperament_tags as a JSONB array on the pets table (e.g., ['gentle', 'good-with-kids', 'house-trained', 'high-energy']). Display them as small tag badges on the pet detail page. For breed-specific information (typical size range, exercise needs, hypoallergenic notes), either add a breeds reference table with a breed lookup page, or include a breed_notes text field directly on the pet row. A pre-seeded breeds table with common dog and cat breeds covering AKC and CFA registrations covers about 95% of shelter animals.

Can adopters submit a full application form with document uploads?

The inquiry form covers the basic contact and message fields. For a full adoption application with document uploads (vet references, landlord letters, ID verification), add file upload inputs that send documents to a private Supabase Storage bucket using a presigned upload URL from an Edge Function. Store the file URLs in an adoption_documents table linked to the inquiry. This turns the simple inquiry into a multi-step application workflow — plan for an additional 6–10 hours of build time with Lovable or V0.

How do I handle multiple photos per pet in a gallery?

The pet_photos table stores one row per photo with a sort_order field. The primary photo (sort_order = 0) appears on the listing card. The full gallery appears on the pet detail page in a carousel (shadcn/ui Carousel on web, photo_view Flutter package on mobile). When a shelter staff member uploads photos via the admin panel, each image goes to Supabase Storage and a new pet_photos row is inserted with an incrementing sort_order. Users can drag to reorder photos in the admin panel.

Is the Petfinder API free for commercial apps?

Yes — Petfinder offers free API access at 1,000 requests per day for any registered developer, including commercial applications. There is no licensing fee or per-call charge within the free tier. For higher volumes, contact Petfinder directly — there is no self-serve paid tier; volume access is negotiated. The free tier is sufficient for a build that syncs pet data to Supabase on a schedule rather than calling Petfinder on every user search.

RapidDev

Need this feature production-ready?

RapidDev builds a pet adoption finder into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.