Trust & Verification - Building User Confidence
The Trust Problem
Food security is personal. When someone is hungry, they need confidence that:
- The food bank exists and is open
- The hours shown are current
- The address is correct
- The services listed are actually available
Bad data doesn't just inconvenience users - it can cause real harm (wasted trips, missed meals).
Phase 3 focused on building user trust through transparency and verification.
Verification Badges
Not all data sources are equal. I created a visual hierarchy:
// src/components/foodshare/verification-badge.tsx
export function VerificationBadge({ status, lastVerified }: Props) {
const badges = {
official: {
label: "Official",
icon: <ShieldCheck className="h-3 w-3" />,
color: "text-green-600 bg-green-50 border-green-200",
description: "Verified by organization staff",
},
community_verified: {
label: "Community Verified",
icon: <Users className="h-3 w-3" />,
color: "text-blue-600 bg-blue-50 border-blue-200",
description: "Confirmed by multiple community members",
},
ai_discovered: {
label: "AI Discovered",
icon: <Sparkles className="h-3 w-3" />,
color: "text-purple-600 bg-purple-50 border-purple-200",
description: "Found by automated discovery, pending verification",
},
pending: {
label: "Pending",
icon: <Clock className="h-3 w-3" />,
color: "text-gray-600 bg-gray-50 border-gray-200",
description: "Awaiting verification",
},
};
const badge = badges[status] || badges.pending;
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge variant="outline" className={badge.color}>
{badge.icon}
<span className="ml-1">{badge.label}</span>
</Badge>
</TooltipTrigger>
<TooltipContent>
<p>{badge.description}</p>
{lastVerified && (
<p className="text-xs mt-1">
Last verified {formatDistanceToNow(lastVerified)} ago
</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
Users could see at a glance the data provenance.
Source Attribution
Transparency required showing where data came from:
-- Schema update
ALTER TABLE food_banks
ADD COLUMN sources JSONB,
ADD COLUMN discovery_method TEXT,
ADD COLUMN discovered_by TEXT,
ADD COLUMN admin_verified_at TIMESTAMP;
// src/components/foodshare/sources-section.tsx
export function SourcesSection({ resource }: Props) {
return (
<Accordion type="single" collapsible>
<AccordionItem value="sources">
<AccordionTrigger>
<div className="flex items-center gap-2">
<ExternalLink className="h-4 w-4" />
Data Sources
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
{resource.sources?.map((source, idx) => (
<div key={idx} className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{getFriendlySourceName(source)}
</span>
<Button asChild variant="ghost" size="sm">
<a href={source} target="_blank" rel="noopener noreferrer">
View
<ExternalLink className="ml-1 h-3 w-3" />
</a>
</Button>
</div>
))}
<Separator className="my-2" />
<p className="text-xs text-muted-foreground">
Discovery: {resource.discoveryMethod || "Manual"}
</p>
{resource.adminVerifiedAt && (
<p className="text-xs text-muted-foreground">
Verified {formatDistanceToNow(resource.adminVerifiedAt)} ago
</p>
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
This section showed:
- Original source URLs (Feeding America, 211, etc.)
- How the resource was discovered (Tavily scan, manual entry, user submission)
- When it was last verified
Data Completeness Indicator
Users needed to know if a resource had complete information:
// src/components/foodshare/completeness-indicator.tsx
export function CompletenessIndicator({ resource }: Props) {
const fields = [
{ label: "Phone", value: resource.phone },
{ label: "Website", value: resource.website },
{ label: "Hours", value: resource.hours },
{ label: "Description", value: resource.description },
{ label: "Services", value: resource.services?.length },
];
const filled = fields.filter((f) => f.value != null).length;
const percentage = Math.round((filled / fields.length) * 100);
const color =
percentage >= 80
? "bg-green-500"
: percentage >= 50
? "bg-yellow-500"
: "bg-gray-300";
return (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Data Completeness</span>
<span className="font-medium">{percentage}%</span>
</div>
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
<div
className={`h-full ${color} transition-all duration-300`}
style={{ width: `${percentage}%` }}
/>
</div>
<Accordion type="single" collapsible>
<AccordionItem value="details" className="border-none">
<AccordionTrigger className="text-xs text-muted-foreground py-2">
View Details
</AccordionTrigger>
<AccordionContent>
<div className="space-y-1">
{fields.map((field) => (
<div key={field.label} className="flex items-center gap-2 text-xs">
{field.value ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<X className="h-3 w-3 text-gray-400" />
)}
<span>{field.label}</span>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}
This visual indicator helped users choose between multiple nearby resources - preferring those with complete data.
Map Deep Linking
Trust required verifiability. Users needed to see resources on a map:
// Community widget linking to map
<Button asChild variant="outline">
<Link href={`/map?resource=${resource.id}`}>
<MapPin className="h-4 w-4 mr-2" />
View on Map
</Link>
</Button>
The map page supported deep linking:
// src/app/map/pageClient.tsx
export default function MapPageClient({ initialResources }: Props) {
const searchParams = useSearchParams();
const resourceId = searchParams.get('resource');
useEffect(() => {
if (resourceId) {
const resource = initialResources.find((r) => r.id === resourceId);
if (resource) {
// Auto-select and center on resource
setSelectedResource(resource.id);
setCenter([resource.longitude, resource.latitude]);
setZoom(15); // Zoom in to street level
}
}
}, [resourceId]);
// ...
}
Clicking "View on Map" auto-centered and selected the resource - instant context.
Community Resource Widget
The community page showed nearby resources:
// src/app/community/components/sidebar/resources-near-you.tsx
export async function ResourcesNearYou() {
const coords = await getUserLocation(); // From session or IP
const nearby = await searchFoodBanks({
latitude: coords.lat,
longitude: coords.lng,
radiusMiles: 5,
limit: 5,
});
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Resources Near You</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{nearby.map((resource) => (
<div
key={resource.id}
className="flex items-start gap-3 p-2 rounded hover:bg-muted/50 transition"
>
<MapPin className="h-4 w-4 mt-0.5 text-muted-foreground" />
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{resource.name}</p>
<p className="text-xs text-muted-foreground">
{calculateDistance(coords.lat, coords.lng, resource.latitude, resource.longitude).toFixed(1)} miles
</p>
</div>
<VerificationBadge status={resource.verificationStatus} />
</div>
))}
<Button asChild variant="outline" className="w-full">
<Link href="/map">
View All on Map
</Link>
</Button>
</CardContent>
</Card>
);
}
This widget surfaced trusted, nearby resources without leaving the community page.
What Went Right
-
Verification Badges: Clear visual hierarchy built trust instantly
-
Source Attribution: Transparency about data origin reduced skepticism
-
Completeness Indicators: Helped users choose between resources
-
Deep Linking: Made resources verifiable across pages
-
Community Integration: Resources widget connected discovery and community
What I'd Do Differently
Mistake 1: No User Contribution Flow
Users couldn't suggest updates or corrections. This required admin intervention for every error.
Mistake 2: Static Verification Status
Verification didn't expire. A resource verified 6 months ago might be outdated.
Mistake 3: No "Report Issue" Flow
Users who found incorrect data had no way to flag it easily.
What I Learned
-
Trust Is Earned Visually: Badges and indicators matter more than disclaimers
-
Transparency Reduces Skepticism: Showing sources built credibility
-
Completeness Matters: Users prefer detailed resources over numerous sparse ones
-
Deep Linking Enables Verification: Let users confirm data themselves
-
Integration Beats Isolation: Resources everywhere (map, community, chat) increased usage
Up Next
In Part 10, I'll cover production deployment to Vercel - database configuration, authentication fixes, and going live.
Key Commits: Phase 3 core features
Related Files:
src/components/foodshare/verification-badge.tsx- Verification badgesrc/components/foodshare/sources-section.tsx- Source attributionsrc/components/foodshare/completeness-indicator.tsx- Completeness UI
Jordan Hindo
Full-stack Developer & AI Engineer building in public. Exploring the future of agentic coding and AI-generated assets.
Get in touch