Bouw een dierenopvangplatform dat huisdieren écht plaatst
Bouw een Platform voor Dierenasieltoepassing dat Daadwerkelijk Dieren Adopteert
De meeste websites van dierenasiel zijn statische brochures die toevallig een pagina "Bekijk onze dieren" hebben die naar Petfinder linkt. De dieren leven op Petfinder. De donaties gaan via PayPal. De aanvragen komen via e-mail. Het vrijwilligersrooster staat op een whiteboard. De "website" van het asiel is eigenlijk alleen maar een wrapper rond vier losgekoppelde diensten van derden.
Ik heb dit patroon tientallen keren gezien. Een directeur van een asiel vraagt een vrijwilliger-ontwikkelaar om "een website voor ons te maken", en wat ze krijgen is een WordPress-thema met wat stockfoto's, een contactformulier en een ingebedde Petfinder-widget. Het is niet dat de ontwikkelaar het niet uitmaakt -- het is dat niemand hen heeft laten zien hoe een echt asielplatform eruit zou kunnen zien.
Dit is wat een geïntegreerd asielplatform eruitziet -- en hoe je er een bouwt die adoptiepercentages meetbaar verhoogt.
Inhoudsopgave
- Waarom losgekoppelde systemen adoptiepercentages doden
- Overzicht van de architectuur
- Databaseschema: je enkele bron van waarheid
- De adoptieflow: van bladeren tot voorgoed thuis
- Petfinder API-integratie: overal synchroniseren vanuit één plek
- Donatiesysteem met Stripe
- Dashboard voor reële impact
- Beheer van fostering en vrijwilligerswerk
- Aanbevelingen voor technologiestapel
- Deployment en hostingkosten
- Veelgestelde vragen

Waarom losgekoppelde systemen adoptiepercentages doden
Laat me je schetsen wat er gebeurt bij een typisch asiel wanneer iemand een dier wil adopteren.
Een potentiële adoptant vindt een hond op Petfinder. Ze klikken door naar de website van het asiel -- die al dan niet bijgewerkte informatie over die hond bevat. Ze vullen een Google Form-aanvraag in. Die aanvraag belandt in iemands Gmail-inbox. De vrijwilligercoördinator ziet het drie dagen later, print het uit en stopt het in een fysieke map. Intussen heeft iemand anders die hond gisteren al geadopteerd. De aanvrager hoort nooit iets terug.
Dit is geen overdrijving. De ASPCA schat dat jaarlijks ongeveer 6,3 miljoen gezelschapdieren Amerikaanse asiel binnenkomen. Ongeveer 4,1 miljoen worden geadopteerd. Die kloof van 2,2 miljoen dieren vertegenwoordigt een systeemfalen, en een aanzienlijk deel daarvan komt neer op operationele wrijving -- niet op een gebrek aan mensen die willen adopteren.
Elk uur vertraging tussen "Ik wil deze hond" en "hier is je adoptietermijn" verlaagt de kans dat de adoptie wordt voltooid. Een 2023 Shelter Animals Count-rapport vond dat asielen met geïntegreerde digitale adoptieflows 23% hogere voltooiingspercentages voor adoptie zagen in vergelijking met die met losgekoppelde tools.
De oplossing is geen ingewikkelde AI of blockchain-onzin. Het is basisarchitectuur voor software: één database, geautomatiseerde workflows en updates in real-time.
Overzicht van de architectuur
Hier is de high-level architectuur voor een asielplatform dat daadwerkelijk werkt:
┌─────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ Public Site │ Adoption Portal │ Admin Dashboard │
└───────┬───────┴────────┬──────────┴───────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ API Layer (Server Actions) │
│ + Supabase Edge Functions │
└───────┬───────┬────────┬──────────┬──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌────────┐ ┌──────────────┐
│ Supabase │ │ Stripe │ │Resend │ │ Petfinder │
│ Postgres │ │ API │ │ Email │ │ Adopt-a-Pet │
│ + Auth │ │ │ │ │ │ RescueGroups │
└──────────┘ └────────┘ └────────┘ └──────────────┘
Het kernprincipe: je Supabase PostgreSQL-database is de enkele bron van waarheid. Alles stroomt er vanuit. Petfinder krijgt een sync van. De publieke website leest ervan. Het admin-dashboard schrijft ervan. Stripe webhooks updaten het.
Geen dierenstatus meer in vier verschillende plaatsen updaten.
Databaseschema: je enkele bron van waarheid
Hier is het complete Supabase-schema. Ik heb dit gebouwd en geïtereerd over meerdere asielprojecten. Fork het, wijzig het, breng het uit.
-- Enable UUID generation
create extension if not exists "uuid-ossp";
-- ============================================
-- ANIMALS
-- ============================================
create type animal_species as enum ('dog', 'cat', 'rabbit', 'bird', 'other');
create type animal_status as enum (
'intake', 'available', 'pending', 'foster',
'adopted', 'transferred', 'deceased'
);
create type animal_sex as enum ('male', 'female', 'unknown');
create type animal_size as enum ('small', 'medium', 'large', 'extra_large');
create table animals (
id uuid primary key default uuid_generate_v4(),
name text not null,
species animal_species not null,
breed text,
breed_secondary text,
age_years integer,
age_months integer,
sex animal_sex default 'unknown',
size animal_size,
weight_lbs numeric(5,1),
color text,
description text,
medical_notes text,
behavioral_notes text,
status animal_status default 'intake',
is_spayed_neutered boolean default false,
is_house_trained boolean,
is_good_with_kids boolean,
is_good_with_dogs boolean,
is_good_with_cats boolean,
special_needs text,
photos text[] default '{}',
primary_photo_url text,
intake_date date default current_date,
adoption_date date,
adoption_fee numeric(7,2),
petfinder_id text,
petfinder_last_sync timestamptz,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- ============================================
-- ADOPTION APPLICATIONS
-- ============================================
create type application_status as enum (
'submitted', 'under_review', 'interview_scheduled',
'home_check_scheduled', 'approved', 'denied', 'withdrawn'
);
create table applications (
id uuid primary key default uuid_generate_v4(),
animal_id uuid references animals(id) on delete set null,
applicant_name text not null,
applicant_email text not null,
applicant_phone text,
address_street text,
address_city text,
address_state text,
address_zip text,
housing_type text, -- 'house', 'apartment', 'condo', etc.
owns_or_rents text,
landlord_name text,
landlord_phone text,
has_yard boolean,
yard_fenced boolean,
current_pets text,
previous_pets text,
vet_name text,
vet_phone text,
household_members text,
work_schedule text,
reason_for_adopting text,
experience text,
status application_status default 'submitted',
reviewer_notes text,
interview_date timestamptz,
home_check_date timestamptz,
signature_url text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- ============================================
-- DONORS & DONATIONS
-- ============================================
create type donation_type as enum ('one_time', 'monthly', 'sponsor');
create table donors (
id uuid primary key default uuid_generate_v4(),
name text not null,
email text unique not null,
phone text,
stripe_customer_id text unique,
total_donated numeric(10,2) default 0,
first_donation_date date,
last_donation_date date,
created_at timestamptz default now()
);
create table donations (
id uuid primary key default uuid_generate_v4(),
donor_id uuid references donors(id),
animal_id uuid references animals(id) on delete set null,
amount numeric(10,2) not null,
donation_type donation_type not null,
stripe_payment_id text,
stripe_subscription_id text,
receipt_sent boolean default false,
thank_you_sent boolean default false,
created_at timestamptz default now()
);
-- ============================================
-- FOSTERS
-- ============================================
create type foster_status as enum ('active', 'inactive', 'on_break');
create table fosters (
id uuid primary key default uuid_generate_v4(),
user_id uuid references auth.users(id),
name text not null,
email text not null,
phone text,
address text,
status foster_status default 'active',
max_capacity integer default 1,
species_preference animal_species[],
size_preference animal_size[],
accepts_special_needs boolean default false,
notes text,
created_at timestamptz default now()
);
create table foster_assignments (
id uuid primary key default uuid_generate_v4(),
foster_id uuid references fosters(id),
animal_id uuid references animals(id),
start_date date default current_date,
end_date date,
notes text,
created_at timestamptz default now()
);
-- ============================================
-- VOLUNTEERS
-- ============================================
create table volunteers (
id uuid primary key default uuid_generate_v4(),
user_id uuid references auth.users(id),
name text not null,
email text not null,
phone text,
emergency_contact text,
skills text[],
availability jsonb, -- {"monday": ["morning", "afternoon"], ...}
total_hours_logged numeric(8,1) default 0,
is_active boolean default true,
onboarded_at date,
created_at timestamptz default now()
);
create table volunteer_shifts (
id uuid primary key default uuid_generate_v4(),
volunteer_id uuid references volunteers(id),
shift_date date not null,
start_time time not null,
end_time time,
hours_logged numeric(4,1),
role text, -- 'dog_walking', 'cleaning', 'front_desk', etc.
notes text,
created_at timestamptz default now()
);
-- ============================================
-- EVENTS
-- ============================================
create table events (
id uuid primary key default uuid_generate_v4(),
title text not null,
description text,
event_date timestamptz not null,
end_date timestamptz,
location text,
location_url text,
max_attendees integer,
is_public boolean default true,
cover_image_url text,
created_at timestamptz default now()
);
create table event_rsvps (
id uuid primary key default uuid_generate_v4(),
event_id uuid references events(id) on delete cascade,
name text not null,
email text not null,
guests integer default 1,
created_at timestamptz default now(),
unique(event_id, email)
);
-- ============================================
-- INDEXES
-- ============================================
create index idx_animals_status on animals(status);
create index idx_animals_species on animals(species);
create index idx_applications_status on applications(status);
create index idx_applications_animal on applications(animal_id);
create index idx_donations_donor on donations(donor_id);
create index idx_donations_created on donations(created_at);
create index idx_foster_assignments_active
on foster_assignments(foster_id) where end_date is null;
-- ============================================
-- ROW LEVEL SECURITY
-- ============================================
alter table animals enable row level security;
alter table applications enable row level security;
alter table donors enable row level security;
alter table donations enable row level security;
-- Public can view available animals
create policy "Animals are viewable by everyone" on animals
for select using (status in ('available', 'pending', 'adopted'));
-- Authenticated staff can do everything with animals
create policy "Staff can manage animals" on animals
for all using (auth.role() = 'authenticated');
-- Applicants can insert their own applications
create policy "Anyone can submit applications" on applications
for insert with check (true);
-- Applicants can view their own applications
create policy "Applicants can view own applications" on applications
for select using (applicant_email = auth.jwt()->>'email');
-- ============================================
-- UPDATED_AT TRIGGER
-- ============================================
create or replace function update_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
create trigger animals_updated_at before update on animals
for each row execute function update_updated_at();
create trigger applications_updated_at before update on applications
for each row execute function update_updated_at();
Dat is je basis. Elke functie die we van hier af bespreken leest uit of schrijft naar deze tabellen.

De adoptieflow: van bladeren tot voorgoed thuis
De adoptieverkoop heeft zeven verschillende fasen. Elk ervan moet in je database worden bijgehouden, zichtbaar zijn op het admin-dashboard en -- waar mogelijk -- worden geautomatiseerd.
Fase 1: Bladeren en zoeken
De openbare dierenengalerie is de meest belangrijke pagina op je site. Niet de startpagina. Niet de pagina "Over ons". De dierenengalerie.
Bouw het met filters die voor adoptanten van belang zijn:
// app/animals/page.tsx
import { createClient } from '@/lib/supabase/server';
interface SearchParams {
species?: string;
size?: string;
good_with_kids?: string;
good_with_dogs?: string;
age?: string; // 'puppy', 'young', 'adult', 'senior'
}
export default async function AnimalsPage({
searchParams
}: {
searchParams: SearchParams
}) {
const supabase = createClient();
let query = supabase
.from('animals')
.select('*')
.in('status', ['available', 'pending'])
.order('created_at', { ascending: false });
if (searchParams.species) {
query = query.eq('species', searchParams.species);
}
if (searchParams.size) {
query = query.eq('size', searchParams.size);
}
if (searchParams.good_with_kids === 'true') {
query = query.eq('is_good_with_kids', true);
}
if (searchParams.good_with_dogs === 'true') {
query = query.eq('is_good_with_dogs', true);
}
if (searchParams.age === 'puppy') {
query = query.lte('age_years', 1);
} else if (searchParams.age === 'senior') {
query = query.gte('age_years', 7);
}
const { data: animals } = await query;
return (
// Your gallery component with filter sidebar
<AnimalGallery animals={animals} />
);
}
Kritiek UX-detail: toon een "In afwachting" badge op dieren met actieve aanvragen. Dit creëert urgentie zonder druk. Een adoptant die "2 aanvragen in afwachting" ziet op een hond waar ze van houden, zal sneller aanvragen.
Fase 2: Profielpagina van dier
Elk dier krijgt een eigen pagina met een unieke URL. Dit is belangrijk voor SEO (mensen zoeken "adopteer Golden Retriever [stadsnaam]") en voor deelbaarheid op sociale media.
Zet: een fotogalerie (geen enkele foto), karakterbeschrijving geschreven in de eerste persoon vanuit het perspectief van het dier (asielen die dit doen zien meer betrokkenheid), medische status, compatibiliteitsinformatie en een opvallende knop "Aanvraag tot adoptie".
Fase 3: Indiening van aanvraag
Gebruik een Next.js Server Action om formulierinzending af te handelen:
// app/actions/submit-application.ts
'use server'
import { createClient } from '@/lib/supabase/server';
import { resend } from '@/lib/resend';
export async function submitApplication(formData: FormData) {
const supabase = createClient();
const application = {
animal_id: formData.get('animal_id'),
applicant_name: formData.get('name'),
applicant_email: formData.get('email'),
applicant_phone: formData.get('phone'),
housing_type: formData.get('housing_type'),
owns_or_rents: formData.get('owns_or_rents'),
has_yard: formData.get('has_yard') === 'true',
current_pets: formData.get('current_pets'),
reason_for_adopting: formData.get('reason'),
// ... rest of fields
};
const { data, error } = await supabase
.from('applications')
.insert(application)
.select()
.single();
if (error) throw new Error('Failed to submit application');
// Update animal status to 'pending' if first application
await supabase
.from('animals')
.update({ status: 'pending' })
.eq('id', application.animal_id)
.eq('status', 'available');
// Send confirmation email
await resend.emails.send({
from: 'adoptions@yourshelter.org',
to: application.applicant_email,
subject: 'Application Received!',
react: ApplicationConfirmationEmail({
applicantName: application.applicant_name,
animalName: data.animal_name
}),
});
// Notify shelter staff
await resend.emails.send({
from: 'system@yourshelter.org',
to: 'adoptions@yourshelter.org',
subject: `New Application: ${data.animal_name}`,
react: StaffNotificationEmail({ application: data }),
});
return { success: true, applicationId: data.id };
}
Fasen 4-7: Review tot adoptie
Het admin-dashboard (beschermd achter Supabase Auth) toont aanvragen in een Kanban-achtig bord: Ingediend → In behandeling → Interview gepland → Huisbezoek → Goedgekeurd/Afgewezen.
Als een aanvraag naar "Goedgekeurd" gaat:
- De status van het dier wordt bijgewerkt naar "geadopteerd"
- De adoptatiedatum wordt geregistreerd
- Een e-handtekeningslink wordt verstuurd (gebruik DocuSign API of een eenvoudiger oplossing zoals SignWell)
- Alle andere hangende aanvragen voor dat dier ontvangen een voorzichtige e-mail met afwijzing
- Een concept voor een adoptieverhaaltje uit successen wordt automatisch gegenereerd voor de blog
Dat laatste punt is meer waard dan je denkt. Verhalen over adoptievoorslagen zijn de inhoud die het best presteert op een website van asielen. Ze stimuleren donaties, inspireren andere adoptanten en presteren goed in lokale SEO.
Petfinder API-integratie: overal synchroniseren vanuit één plek
Dit is het onderdeel dat het meeste handmatige werk elimineert. In plaats van je aan te melden bij Petfinder, Adopt-a-Pet en RescueGroups apart om aanbiedingen bij te werken, beheer je alles in je database en synchroniseer je naar buiten.
// supabase/functions/sync-petfinder/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const PETFINDER_API_KEY = Deno.env.get('PETFINDER_API_KEY')!;
const PETFINDER_SECRET = Deno.env.get('PETFINDER_SECRET')!;
serve(async () => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// Get Petfinder OAuth token
const tokenRes = await fetch('https://api.petfinder.com/v2/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: PETFINDER_API_KEY,
client_secret: PETFINDER_SECRET,
}),
});
const { access_token } = await tokenRes.json();
// Fetch animals that need syncing
const { data: animals } = await supabase
.from('animals')
.select('*')
.in('status', ['available', 'pending'])
.or(
`petfinder_last_sync.is.null,` +
`petfinder_last_sync.lt.${new Date(Date.now() - 20 * 60 * 1000).toISOString()}`
);
for (const animal of animals || []) {
// Map your schema to Petfinder's format
const petfinderData = {
type: animal.species === 'dog' ? 'Dog' : 'Cat',
breed: { primary: animal.breed, secondary: animal.breed_secondary },
age: mapAgeToPetfinder(animal.age_years),
gender: animal.sex === 'male' ? 'Male' : 'Female',
size: animal.size?.charAt(0).toUpperCase() + animal.size?.slice(1),
name: animal.name,
description: animal.description,
photos: animal.photos,
status: animal.status === 'available' ? 'adoptable' : 'found',
attributes: {
spayed_neutered: animal.is_spayed_neutered,
house_trained: animal.is_house_trained,
special_needs: !!animal.special_needs,
},
environment: {
children: animal.is_good_with_kids,
dogs: animal.is_good_with_dogs,
cats: animal.is_good_with_cats,
},
};
// Push to Petfinder (or update existing)
// Note: Petfinder's publisher API requires shelter partnership
await syncToPetfinder(access_token, animal.petfinder_id, petfinderData);
// Update sync timestamp
await supabase
.from('animals')
.update({ petfinder_last_sync: new Date().toISOString() })
.eq('id', animal.id);
}
// Remove adopted animals from Petfinder
const { data: adopted } = await supabase
.from('animals')
.select('petfinder_id')
.eq('status', 'adopted')
.not('petfinder_id', 'is', null);
for (const animal of adopted || []) {
await removePetfinderListing(access_token, animal.petfinder_id);
}
return new Response(JSON.stringify({ synced: animals?.length || 0 }));
});
Stel deze Edge-functie in om op basis van een cron-schema elke 20 minuten te worden uitgevoerd met behulp van Supabase's pg_cron of een externe scheduler. Je aanbiedingen blijven vers op alle platforms zonder dat iemand Petfinder direct aanraakt.
Donatiesysteem met Stripe
Asielen laten geld liggen met basisknop PayPal. Hier is een donatiesysteem met drie niveaus dat daadwerkelijk werkt:
| Donatiesoort | Bedragen | Stripe-functie | Donor-voordeel |
|---|---|---|---|
| Eenmalig | $25, $50, $100, aangepast | Checkout Session | E-mail met belastingkwitantie |
| Maandelijks terugkerend | $10, $25, $50/mnd | Abonnementen | Maandelijkse update van impact |
| Sponsor een dier | $25/mnd gekoppeld aan animal_id | Abonnement + metadata | Maandelijkse foto-update van hun gesponsorde dier |
De "Sponsor een dier" rij is de geldmaker. Ik heb asielen gezien die maandelijks $3.000-5.000 terugkerende inkomsten genereren door dit alleen. Mensen betalen graag $25/maand om maandelijkse e-maile met foto's te krijgen van "hun" hond of kat terwijl het wacht op adoptie.
// app/api/donate/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { amount, type, animalId, email } = await request.json();
if (type === 'one_time') {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: { name: 'Donation to [Shelter Name]' },
unit_amount: amount * 100,
},
quantity: 1,
}],
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_URL}/donate/thank-you`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/donate`,
customer_email: email,
metadata: { type: 'one_time', animal_id: animalId || '' },
});
return Response.json({ url: session.url });
}
if (type === 'monthly' || type === 'sponsor') {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: type === 'sponsor'
? `Sponsor: ${animalId}`
: 'Monthly Donation'
},
unit_amount: amount * 100,
recurring: { interval: 'month' },
},
quantity: 1,
}],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_URL}/donate/thank-you`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/donate`,
customer_email: email,
metadata: { type, animal_id: animalId || '' },
});
return Response.json({ url: session.url });
}
}
De Stripe webhook-handler schrijft elke donatie naar je database, verstuurt een bedankings-e-mail met Resend en genereert een belastingkwitantie. Als een gesponsord dier wordt geadopteerd, ontvangt de sponsor een speciale "graduatie" e-mail en wordt aangespoord om hun abonnement af te zeggen of over te zetten naar een ander dier.
Dashboard voor reële impact
Dit is je openbare-facing geloofwaardigheidspagina. Zet het op /impact en toon live statistieken:
// app/impact/page.tsx
import { createClient } from '@/lib/supabase/server';
export const revalidate = 60; // ISR: revalidate every 60 seconds
export default async function ImpactPage() {
const supabase = createClient();
const [rescued, adopted, inCare, fosters, monthlyDonations] =
await Promise.all([
supabase
.from('animals')
.select('id', { count: 'exact', head: true })
.neq('status', 'intake'),
supabase
.from('animals')
.select('id', { count: 'exact', head: true })
.eq('status', 'adopted'),
supabase
.from('animals')
.select('id', { count: 'exact', head: true })
.in('status', ['available', 'pending', 'foster']),
supabase
.from('fosters')
.select('id', { count: 'exact', head: true })
.eq('status', 'active'),
supabase
.from('donations')
.select('amount')
.gte('created_at', new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString()),
]);
const totalDonationsThisMonth = monthlyDonations.data
?.reduce((sum, d) => sum + Number(d.amount), 0) || 0;
return (
<div className="grid grid-cols-2 md:grid-cols-5 gap-6">
<StatCard label="Animals Rescued" value={rescued.count || 0} />
<StatCard label="Adopted" value={adopted.count || 0} />
<StatCard label="Currently In Care" value={inCare.count || 0} />
<StatCard label="Active Foster Homes" value={fosters.count || 0} />
<StatCard
label="Donations This Month"
value={`$${totalDonationsThisMonth.toLocaleString()}`}
/>
</div>
);
}
Voor real-time updates aan de clientzijde, gebruik Supabase's Realtime-abonnementen. Wanneer iemand doneert, telt de teller live op. Het is een klein detail dat een grote psychologische impact maakt.
Beheer van fostering en vrijwilligerswerk
Foster-beheer is waar de meeste asielsoftware uit elkaar valt. Het matchingprobleem -- welk foster-huis is geschikt voor welk dier -- leeft meestal in iemands hoofd.
Met je database kun je matching automatiseren:
-- Find available fosters for a specific animal
select f.* from fosters f
where f.status = 'active'
and f.max_capacity > (
select count(*) from foster_assignments fa
where fa.foster_id = f.id and fa.end_date is null
)
and 'dog' = any(f.species_preference)
and 'large' = any(f.size_preference)
and (f.accepts_special_needs = true or 'none' = $special_needs)
order by f.max_capacity - (
select count(*) from foster_assignments fa
where fa.foster_id = f.id and fa.end_date is null
) desc;
Vrijwilligerplanningen vervangen het whiteboard. Vrijwilligers loggen in, zien beschikbare diensten, claimen ze en registreren hun uren. Asielmanagers krijgen een real-time zicht op coverage.
Aanbevelingen voor technologiestapel
| Laag | Gereedschap | Waarom |
|---|---|---|
| Frontend | Next.js 15 (App Router) | SSR voor SEO, Server Actions voor formulieren, ISR voor prestaties |
| Database | Supabase (PostgreSQL) | Gratis laag behandelt de meeste asielen, ingebouwde auth, real-time, edge-functies |
| Betalingen | Stripe | Industrienorm, abonnementsondersteuning, webhook-betrouwbaarheid |
| Resend | React-e-mailtemplates, uitstekende DX, betaalbaar ($20/mnd voor 50k e-mails) | |
| Afbeeldingenhosting | Cloudinary of Supabase Storage | Automatische optimalisatie, responsive afbeeldingen |
| E-handtekeningen | SignWell of DocuSeal (zelf gehost) | DocuSeal is gratis en open source |
| Hosting | Vercel | Gratis laag werkt voor de meeste asielen, automatische previews |
| Analytics | Plausible of PostHog | Privacy-first, geen cookie-banners nodig |
Als je Astro overweegt in plaats van Next.js -- in het bijzonder voor asielen die niet veel interactiviteit nodig hebben op de openbare site -- bekijk onze Astro-ontwikkelingsmogelijkheden. Voor de admin-dashboardonderdelen die meer dynamische functionaliteit nodig hebben, is Next.js nog steeds moeilijk te verslaan.
Deployment en hostingkosten
Hier is de realiteitscheck. Een asiel dat deze stack draait zal uitgeven:
| Service | Gratis laag | Betaalde laag (indien nodig) |
|---|---|---|
| Supabase | 500MB database, 50k auth-gebruikers | $25/mnd (Pro) |
| Vercel | 100GB bandbreedte | $20/mnd (Pro) |
| Stripe | Geen maandelijks tarief | 2,9% + $0,30 per transactie |
| Resend | 3.000 e-mails/mnd gratis | $20/mnd |
| Cloudinary | 25GB opslag gratis | $89/mnd (Plus) |
| Domein | -- | $12/jaar |
| Totaal (gratis laag) | $1/mnd (alleen domein) | -- |
| Totaal (betaalde lagen) | -- | ~$65-155/mnd |
Vergelijk dit met ShelterLuv ($150+/mnd), PetPoint (bedrijfsprijzen) of Shelterbuddy ($200+/mnd). Je krijgt meer flexibiliteit, je bent eigenaar van je gegevens en geeft minder uit. Het compromis is dat je een ontwikkelaar nodig hebt om het te bouwen en onderhouden -- maar daarom bestaan vrijwilliger-ontwikkelaars en bureaus zoals Social Animal.
Voor organisaties die deze architectuur willen zonder zelf te bouwen, bieden we headless CMS-ontwikkelingoplossingen die de volledige installatiegroep van het asielplatform bevatten.
Veelgestelde vragen
Hoe lang duurt het om vanaf nul een dieradoptieplatform te bouwen? Met het schema en de architectuur die hier worden beschreven, kan een bekwame ontwikkelaar een MVP in 4-6 weken deeltijds uitbrengen. De openbare dierenengalerie, het aanvraagformulier en het admin-dashboard zijn de minimale haalbare functies. Stripe-integratie voegt nog een week toe. Petfinder-synchronisatie voegt 2-3 dagen toe. Budget 8-12 weken voor een volledig uitgerust platform met beheer van vrijwilligers en foster-matching.
Kan ik deze architectuur gebruiken met een bestaand asielbehersysteem zoals ShelterLuv? Absoluut. Als je al ShelterLuv of PetPoint als je primaire dierenbehersysteem gebruikt, kun je hun API als je gegevensbron behandelen in plaats van dieren rechtstreeks in Supabase te beheren. De Supabase-database wordt dan een synchronisatielaag -- haal dierengegevens uit ShelterLuv, verrijk het met je eigen velden (aangepaste beschrijvingen, extra foto's) en push het naar je website en Petfinder. Je krijgt het beste van beide werelden.
Is Supabase betrouwbaar genoeg voor een asiel dat honderden aanvragen per maand verwerkt? De gratis laag van Supabase verwerkt 500MB gegevens en onbeperkte API-aanvragen. Een asiel dat 500 aanvragen per maand verwerkt met 200 actieve dieren komt niet in de buurt van deze limieten. Supabase's infrastructuur draait op AWS en heeft 99,9% uptime SLA op betaalde plannen. Ter referentie: Supabase had in begin 2025 meer dan 1 miljoen databases draaien. Het is gereed voor productie.
Hoe werkt de Petfinder API voor asielen? Petfinder biedt twee API-producten. De openbare API (v2) laat je dierenaanbiedingen lezen. De Publisher API laat partnerorganisaties hun aanbiedingen programmatisch pushen en updaten. Je moet je aanmelden als Petfinder-partnerorganisatie (gratis) en goedkeuring krijgen voor publisher-toegang. Zodra goedgekeurd, ontvang je API-inloggegevens waarmee je aanbiedingen kunt maken, bijwerken en verwijderen. De synchronisatiefunctie die in dit artikel wordt beschreven, maakt gebruik van de Publisher API.
Wat te doen met GDPR en gegevensprivacy voor adoptieaanvragers? Sla alleen op wat je nodig hebt, verwijder wat je niet nodig hebt. Aanvragen die zijn afgewezen of ingetrokken, moeten persoonsgegevens na 90 dagen verwijderen (anonieme records voor statistieken behouden). Rijen Supabase Level Security-beleid zorgt ervoor dat aanvragers alleen hun eigen aanvragen kunnen zien. Voeg een duidelijk privacybeleid toe waarin je uitlegt welke gegevens je verzamelt en waarom. Voor Amerikaanse asielen ben je niet onderworpen aan GDPR tenzij je EU-inwonergegevens verwerkt, maar Californië's CCPA kan van toepassing zijn als je gegevens van Californische inwoners verwerkt.
Hoe ga ik efficiënt om met dierenfotos? Gebruik Cloudinary of Supabase Storage met afbeeldingstransformaties. Sla de originele upload op en genereer vervolgens geoptimaliseerde versies on-the-fly: 400x400 miniaturen voor het galerij-grid, 800px breed voor profielpagina's en originele resolutie voor downloaden. Cloudinary's gratis laag geeft je 25GB opslag en 25GB bandbreedte per maand. Voor een asiel met 200 dieren en 5 foto's elk, dat is grofweg 2-3GB opslag -- goed binnen de gratis limieten.
Kunnen vrijwilliger-ontwikkelaars dit realistisch onderhouden? Dit is het eerlijke risico. Het omzetingspercentage van vrijwilliger-ontwikkelaars is hoog bij non-profits. De architectuur maakt opzettelijk gebruik van mainstream-tools (Next.js, PostgreSQL, Stripe) in plaats van niche-frameworks, zodat de volgende ontwikkelaar het kan oppikken. Documenteer alles. Gebruik TypeScript voor typeveiligheid. Schrijf databasemigraties in plaats van handmatige schemawijzigingen. Als je op een punt aankomt waar je professionele ondersteuning nodig hebt, kunnen bureaus die gespecialiseerd zijn in Next.js-ontwikkeling zonder volledige herbouw invoelen.
Wat is de meetbare impact van een geïntegreerd asielplatform versus losgekoppelde tools? Asielen die zijn overgegaan van losgekoppelde Petfinder + Google Forms + PayPal-instellingen naar geïntegreerde platforms melden 20-35% snellere verwerkingstijden voor aanvragen (van gemiddeld 5-7 dagen tot 2-3 dagen), 15-25% stijgingen in voltooide adoptie (minder aanvragers vallen weg tijdens het proces) en 40-60% stijgingen in online donatieopbrengsten -- vooral afkomstig van terugkerende sponsorships, die helemaal niet bestaan in een PayPal-knopinstelling. De getallen variëren per asielmaat en locatie, maar de richting is consistent: minder wrijving betekent meer adoptie.
Moet ik alles tegelijk bouwen of kan ik klein beginnen? Begin klein. Fase 1: dierenengalerie + aanvraagformulier + admin-dashboard. Dat alleen elimineert al het grootste frictiepunt. Fase 2: Stripe-donaties met sponsorships. Fase 3: Petfinder-synchronisatie. Fase 4: beheer van vrijwilligers en foster. Elke fase is onafhankelijk waardevol. Laat perfect niet de vijand van verzonden zijn.