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

- Tool: App Features
- Last updated: July 2026

## 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.

## 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.

- **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.
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.

## 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.

```sql
create table public.pets (
  id uuid primary key default gen_random_uuid(),
  shelter_id uuid,
  name text not null,
  species text not null,
  breed text,
  age_months int not null default 0,
  gender text check (gender in ('male', 'female', 'unknown')),
  size text check (size in ('small', 'medium', 'large', 'extra-large')),
  status text not null default 'available' check (status in ('available', 'pending', 'adopted')),
  description text,
  location_lat decimal(9,6),
  location_lng decimal(9,6),
  temperament_tags jsonb not null default '[]',
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table public.pet_photos (
  id uuid primary key default gen_random_uuid(),
  pet_id uuid references public.pets(id) on delete cascade not null,
  image_url text not null,
  sort_order int not null default 0
);

create table public.user_favorites (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  pet_id uuid references public.pets(id) on delete cascade not null,
  created_at timestamptz not null default now(),
  unique (user_id, pet_id)
);

create table public.adoption_inquiries (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade,
  pet_id uuid references public.pets(id) on delete cascade not null,
  adopter_name text not null,
  adopter_email text not null,
  adopter_phone text,
  message text,
  status text not null default 'pending' check (status in ('pending', 'reviewed', 'approved', 'declined')),
  created_at timestamptz not null default now(),
  unique (user_id, pet_id)
);

-- Enable RLS
alter table public.pets enable row level security;
alter table public.pet_photos enable row level security;
alter table public.user_favorites enable row level security;
alter table public.adoption_inquiries enable row level security;

-- Public read for pet discovery
create policy "Public can view pets"
  on public.pets for select using (true);

create policy "Public can view pet photos"
  on public.pet_photos for select using (true);

-- Favorites: only the owning user
create policy "Users can manage own favorites"
  on public.user_favorites for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- Inquiries: users can insert and view their own
create policy "Users can submit inquiries"
  on public.adoption_inquiries for insert
  with check (auth.uid() = user_id);

create policy "Users can view own inquiries"
  on public.adoption_inquiries for select
  using (auth.uid() = user_id);

-- Indexes
create index pets_species_status_idx on public.pets (species, status);
create index pets_location_idx on public.pets (location_lat, location_lng);
create index pet_photos_pet_sort_idx on public.pet_photos (pet_id, sort_order);
create index user_favorites_user_idx on public.user_favorites (user_id);
create index adoption_inquiries_pet_idx on public.adoption_inquiries (pet_id);
```

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 paths

### Lovable — fit 4/10, 4-6 hours

Best overall path — Lovable handles Supabase CRUD for pet listings, favorites, and the inquiry form in one session, and auto-wires auth for the favorites feature. Map view and the Petfinder API proxy require follow-up prompts and manual Secrets configuration.

1. Create a new Lovable project and connect Lovable Cloud to provision Supabase auth, the four tables, and storage for pet photos
2. Paste the prompt below into Agent Mode to build the filter panel, pet grid, pet profile page, favorites, and inquiry form
3. Open Lovable Cloud tab → Secrets, add MAPBOX_PUBLIC_TOKEN — then send a follow-up prompt: 'Add a Mapbox GL JS map to the discovery page showing shelter location pins; use the MAPBOX_PUBLIC_TOKEN from env vars'
4. Run the SQL schema from this page in the Supabase SQL editor to ensure all constraints and indexes are in place
5. If integrating the Petfinder API, add PETFINDER_CLIENT_ID and PETFINDER_CLIENT_SECRET to Secrets, then prompt Lovable to 'Create a Supabase Edge Function that fetches pets from the Petfinder API v2 using client_credentials OAuth and syncs them to the pets table'
6. Publish and verify: test filter combinations, favorites toggle, map pin interaction, and inquiry form submission

Starter prompt:

```
Build a pet adoption finder app. Supabase tables: pets (id, shelter_id, name, species, breed, age_months int, gender, size, status enum available/pending/adopted, description, location_lat decimal, location_lng decimal, temperament_tags jsonb, created_at), pet_photos (id, pet_id, image_url, sort_order), user_favorites (user_id, pet_id, unique constraint), adoption_inquiries (user_id, pet_id, adopter_name, adopter_email, adopter_phone, message, status). RLS: pets and pet_photos are publicly readable; user_favorites and adoption_inquiries require auth.uid() = user_id. Discovery page: filter panel with species checkboxes (dog, cat, rabbit, bird, other), breed text search, age range dropdown (puppy/kitten, young, adult, senior mapped to age_months ranges), size multi-select (small, medium, large), gender radio. Build the Supabase query dynamically — only add a filter clause for each option that has a value selected. Pet cards: photo from pet_photos sorted by sort_order, name, species, breed, age_months displayed as human-readable string, status badge (green: available, yellow: pending, grey: adopted). Pagination: 12 per page, 'Load more' button. Pet detail page at /pets/[id]: photo carousel, all details, 'Start Adoption Inquiry' button. Inquiry form: react-hook-form capturing adopter_name, adopter_email, adopter_phone, message; on submit write to adoption_inquiries, show success confirmation. Favorites heart icon: optimistic UI update; redirect to login if unauthenticated; unique constraint prevents duplicates. Handle empty search state with 'No pets found matching your filters — try clearing some filters' message and a clear-filters button.
```

Limitations:

- Map view (Mapbox GL JS) requires manually adding MAPBOX_PUBLIC_TOKEN to Lovable Secrets and a follow-up prompt — it is not included in the base build above
- Petfinder API integration requires a Supabase Edge Function proxy — direct browser calls return CORS errors; this is a separate prompt after the base build
- The Mapbox GL JS CSS must be imported in the root component or the map renders without controls — Lovable sometimes misses this import

### V0 — fit 4/10, 5-8 hours

Excellent for the marketing-facing discovery page — Next.js SSR/ISR makes pet listings SEO-indexed for local shelter searches. shadcn/ui filter sidebar and pet card grid look polished. The Petfinder API token refresh requires a Next.js API route that V0 sometimes generates as a client-side call.

1. Prompt V0 with the spec below to build the discovery page, filter sidebar, and pet profile pages
2. In the V0 Vars panel, add NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_MAPBOX_TOKEN, and (for Petfinder) PETFINDER_CLIENT_ID and PETFINDER_CLIENT_SECRET
3. Run the SQL schema from this page in the Supabase SQL editor
4. Ensure the Petfinder API call is in an /api/petfinder route — if V0 generated it as a client-side fetch, move the token exchange to the API route and call the API route from the component
5. In the Next.js layout.tsx, add the Mapbox CSS import: import 'mapbox-gl/dist/mapbox-gl.css'
6. Deploy to Vercel and test the full filter-to-profile-to-inquiry flow on mobile

Starter prompt:

```
Build a pet adoption finder using Next.js App Router, Supabase, shadcn/ui, and Mapbox GL JS. Discovery page at /adopt: shadcn/ui Sheet filter drawer (mobile) and sidebar (desktop) with species checkboxes, breed text input, age range select (puppy/kitten 0-12mo, young 13-36mo, adult 37-96mo, senior 97+mo), size multi-select, gender radio. Build the Supabase query using a dynamic filter builder — only add .eq(), .ilike(), .gte(), .lte() clauses when the filter has a selected value; never pass empty strings as filter values. Pet card grid (CSS Grid, 3 columns desktop, 2 tablet, 1 mobile): primary photo from pet_photos (sort_order ASC, limit 1), name, breed, age_months formatted as human-readable, status badge with color coding. Load more button: 12 per page using Supabase .range(). Pet detail page at /adopt/[id]: shadcn/ui Carousel for pet_photos, details panel, adoption inquiry form (react-hook-form + zod: adopter_name required, adopter_email required email, adopter_phone optional, message required min 10 chars). On valid submit, POST to /api/inquiries which writes to Supabase adoption_inquiries and calls Resend to send a shelter notification email. Map section: Mapbox GL JS (import 'mapbox-gl/dist/mapbox-gl.css' in root layout) with pins at location_lat/location_lng; clicking a pin filters the grid to pets from that shelter. Favorites: heart button, optimistic toggle using useState, Supabase upsert/delete; redirect to /login if not authenticated. SEO: generateMetadata per pet page with pet name and species in title and og:image from primary photo URL.
```

Limitations:

- V0 does not auto-provision Supabase — run the SQL schema manually
- The Petfinder API OAuth token exchange must be in a Next.js API route — if V0 places it in the component, move it manually
- Mapbox GL JS CSS import in the root layout is easy to miss; without it, map controls are invisible and pins may not render correctly

### Flutterflow — fit 4/10, 1-2 days

Strong for a native mobile adoption app where swipe-to-favorite gestures, flutter_map for proximity search, and Firebase Auth for user sessions deliver a polished native experience. Multi-filter query building for AND logic across species, breed, and radius requires a custom Dart function.

1. Connect FlutterFlow to Supabase in Settings → Supabase, importing the pets, pet_photos, user_favorites, and adoption_inquiries tables
2. Build the pet listing page using a FlutterFlow GridView bound to a Supabase query action with species and status filters; add Infinite Scroll for pagination
3. Add the flutter_map package as a Custom Widget to render shelter location pins; use the Mapbox tile URL with your token as a Custom Action
4. For the multi-filter query builder (AND logic across species + breed + radius), write a Custom Dart Function that constructs the Supabase query string dynamically and returns a filtered list
5. Implement the favorites toggle as a Supabase conditional action: check if (user_id, pet_id) exists in user_favorites → if yes, delete; if no, insert; update the heart icon state variable
6. Enable Settings → Permissions: Location (for 'find near me'), and add NSLocationWhenInUseUsageDescription in iOS settings for App Store compliance

Limitations:

- Complex multi-filter query building (AND across species, breed, and radius) is not achievable with FlutterFlow visual query blocks — requires a custom Dart function
- flutter_map integration for the map view requires adding it as a Custom Widget; FlutterFlow's built-in map widget is Google Maps only, which has different attribution requirements
- The Petfinder API OAuth flow (client_credentials) requires a custom Dart HTTP action or a backend proxy — it cannot be built with FlutterFlow's visual REST API integration

### Custom — fit 5/10, 1-3 weeks

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.

1. Next.js App Router with Supabase — ISR for pet listing pages to enable Google indexing of individual pet profiles
2. Scheduled 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. Multi-shelter admin portal: per-shelter Supabase RLS using a shelter_id scoped policy; each shelter's staff can only edit their own pets
4. Adoption 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

Limitations:

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

## Gotchas

- **Petfinder API calls fail in Lovable preview with a CORS error** — 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** — 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** — 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** — 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

- 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
- 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
- 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
- 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
- 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
- Add a unique constraint on (user_id, pet_id) in user_favorites to prevent duplicate favorites and enable upsert-based toggle behavior
- 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
- 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

## 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.

---

Source: https://www.rapidevelopers.com/app-features/pet-adoption-finder
© RapidDev — https://www.rapidevelopers.com/app-features/pet-adoption-finder
