Documentation Index
Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
Use this file to discover all available pages before exploring further.
Success Stories
Real stories from real people who discovered benefits they qualified for, building trust and reducing stigma.
Why Success Stories Matter
Many people don’t apply for benefits because:- They assume “those programs aren’t for people like me”
- They feel ashamed about needing help
- They don’t believe the programs actually work
- Diverse people benefiting (not just stereotypes)
- Concrete impact (dollar amounts, life changes)
- That the process is manageable
Data Model
interface SuccessStory {
id: string;
// Author
authorId: string | null; // null for truly anonymous
authorName: string; // Display name or "Anonymous"
isAnonymous: boolean;
// Content
title: string; // "How SNAP helped my family..."
story: string; // Full narrative (min 100 chars)
quote: string | null; // Pull quote for cards
// Association
programId: string | null;
programName: string | null;
// Impact metrics
benefitReceivedCents: number | null;
timeToApprovalDays: number | null;
// Moderation
status: "pending" | "approved" | "rejected" | "featured";
moderatedBy: string | null;
moderatedAt: Date | null;
rejectionReason: string | null;
// Engagement
viewCount: number;
helpfulCount: number;
// Media
imageUrl: string | null;
videoUrl: string | null;
// Timestamps
createdAt: Date;
updatedAt: Date;
}
Database Schema
CREATE TABLE success_stories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Author (can be anonymous)
author_id TEXT REFERENCES profiles(id) ON DELETE SET NULL,
author_name TEXT NOT NULL,
is_anonymous BOOLEAN DEFAULT false,
-- Story content
title TEXT NOT NULL,
story TEXT NOT NULL,
quote TEXT,
-- Related program
program_id UUID REFERENCES programs(id) ON DELETE SET NULL,
-- Impact metrics
benefit_received_cents INTEGER,
time_to_approval_days INTEGER,
-- Moderation
status TEXT DEFAULT 'pending' CHECK (status IN (
'pending', -- Awaiting review
'approved', -- Published
'rejected', -- Not suitable
'featured' -- Highlighted on homepage
)),
moderated_by TEXT REFERENCES profiles(id) ON DELETE SET NULL,
moderated_at TIMESTAMPTZ,
rejection_reason TEXT,
-- Engagement
view_count INTEGER DEFAULT 0,
helpful_count INTEGER DEFAULT 0,
-- Media
image_url TEXT,
video_url TEXT,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_success_stories_status
ON success_stories(status, created_at DESC)
WHERE status IN ('approved', 'featured');
CREATE INDEX idx_success_stories_program
ON success_stories(program_id, status)
WHERE status IN ('approved', 'featured');
-- Deduplicated voting
CREATE TABLE story_helpful_votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
story_id UUID NOT NULL REFERENCES success_stories(id) ON DELETE CASCADE,
user_id TEXT REFERENCES profiles(id) ON DELETE SET NULL,
session_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(story_id, user_id),
UNIQUE(story_id, session_id)
);
Story Submission Flow
Three-step wizard for submitting stories.// app/(dashboard)/share-story/page.tsx
export default function ShareStoryPage() {
const [step, setStep] = useState<"form" | "review" | "success">("form");
const [formData, setFormData] = useState<StoryFormData>({
title: "",
story: "",
quote: "",
programId: "",
benefitAmount: "",
isAnonymous: false,
displayName: "",
});
const isFormValid =
formData.title.trim() &&
formData.story.trim() &&
formData.story.length >= 100 &&
(formData.isAnonymous || formData.displayName.trim());
if (step === "success") {
return <SuccessScreen />;
}
if (step === "review") {
return (
<ReviewScreen
formData={formData}
onEdit={() => setStep("form")}
onSubmit={async () => {
await submitStory(formData);
setStep("success");
}}
/>
);
}
return (
<div className="max-w-2xl mx-auto">
{/* Hero */}
<div className="text-center mb-8">
<div className="w-16 h-16 rounded-full bg-amber-100 mx-auto mb-4 flex items-center justify-center">
<Sparkles className="h-8 w-8 text-amber-600" />
</div>
<h1 className="text-2xl font-bold">Share Your Success Story</h1>
<p className="text-stone-600 mt-2">
Your experience could help someone else discover benefits they qualify for
</p>
</div>
{/* Form */}
<Card>
<CardContent className="space-y-6 p-6">
{/* Title */}
<div>
<Label>Story Title *</Label>
<Input
placeholder="e.g., How SNAP helped my family during a difficult time"
value={formData.title}
onChange={e => setFormData(f => ({ ...f, title: e.target.value }))}
/>
</div>
{/* Story */}
<div>
<Label>Your Story *</Label>
<p className="text-xs text-stone-500 mb-1.5">
Minimum 100 characters. Share what happened, how you found the
program, and how it helped.
</p>
<Textarea
rows={8}
value={formData.story}
onChange={e => setFormData(f => ({ ...f, story: e.target.value }))}
/>
<p className="text-xs text-stone-500 mt-1">
{formData.story.length} / 100 characters minimum
</p>
</div>
{/* Pull quote */}
<div>
<Label>Pull Quote (Optional)</Label>
<p className="text-xs text-stone-500 mb-1.5">
A short, impactful quote we can feature in cards
</p>
<Textarea
rows={2}
placeholder="e.g., This program changed my family's life..."
value={formData.quote}
onChange={e => setFormData(f => ({ ...f, quote: e.target.value }))}
/>
</div>
{/* Program selection */}
<div>
<Label>Related Program (Optional)</Label>
<Select
value={formData.programId}
onValueChange={v => setFormData(f => ({ ...f, programId: v }))}
>
<SelectTrigger>
<SelectValue placeholder="Select a program..." />
</SelectTrigger>
<SelectContent>
{PROGRAMS.map(p => (
<SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Benefit amount */}
<div>
<Label>Benefit Amount Received (Optional)</Label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-500">
$
</span>
<Input
type="number"
className="pl-7"
value={formData.benefitAmount}
onChange={e => setFormData(f => ({ ...f, benefitAmount: e.target.value }))}
/>
</div>
<p className="text-xs text-stone-500 mt-1">
Monthly benefit or one-time assistance received
</p>
</div>
{/* Anonymous toggle */}
<div className="flex items-center justify-between p-4 bg-stone-50 rounded-lg">
<div>
<p className="font-medium">Share Anonymously</p>
<p className="text-sm text-stone-500">
Your name won't be displayed
</p>
</div>
<Switch
checked={formData.isAnonymous}
onCheckedChange={v => setFormData(f => ({ ...f, isAnonymous: v }))}
/>
</div>
{/* Display name */}
{!formData.isAnonymous && (
<div>
<Label>Display Name *</Label>
<Input
placeholder="e.g., Sarah J."
value={formData.displayName}
onChange={e => setFormData(f => ({ ...f, displayName: e.target.value }))}
/>
<p className="text-xs text-stone-500 mt-1">
You can use first name and last initial
</p>
</div>
)}
{/* Guidelines */}
<div className="bg-blue-50 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2 flex items-center gap-2">
<Heart className="h-4 w-4" />
Story Guidelines
</h4>
<ul className="text-sm text-blue-800 space-y-1">
<li>Be honest and share your real experience</li>
<li>Avoid sensitive info (SSN, account numbers)</li>
<li>Keep it respectful and appropriate</li>
<li>Stories are reviewed before publishing</li>
</ul>
</div>
{/* Submit */}
<Button
className="w-full"
size="lg"
disabled={!isFormValid}
onClick={() => setStep("review")}
>
Preview & Submit
</Button>
</CardContent>
</Card>
</div>
);
}
Story Card Component
Three variants for different display contexts.// components/success-stories/story-card.tsx
interface StoryCardProps {
story: SuccessStory;
variant?: "default" | "compact" | "featured";
onMarkHelpful?: (storyId: string) => void;
hasVoted?: boolean;
}
export function StoryCard({
story,
variant = "default",
onMarkHelpful,
hasVoted = false,
}: StoryCardProps) {
// Compact variant for carousels
if (variant === "compact") {
return (
<Card className="h-full hover:shadow-md transition-shadow">
<CardContent className="p-5">
{story.quote && (
<div className="relative mb-4">
<Quote className="absolute -top-1 -left-1 h-6 w-6 text-stone-200" />
<p className="text-stone-700 italic pl-6 line-clamp-3">
“{story.quote}”
</p>
</div>
)}
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-stone-200 flex items-center justify-center">
{getInitials(story)}
</div>
<div>
<p className="text-sm font-medium">{getDisplayName(story)}</p>
{story.programName && (
<p className="text-xs text-stone-500">{story.programName}</p>
)}
</div>
</div>
{story.benefitReceivedCents && (
<Badge className="mt-3 bg-emerald-100 text-emerald-700">
<DollarSign className="h-3 w-3 mr-1" />
{formatBenefit(story.benefitReceivedCents)} received
</Badge>
)}
</CardContent>
</Card>
);
}
// Featured variant for homepage
if (variant === "featured") {
return (
<Card className="overflow-hidden bg-gradient-to-br from-stone-50 to-white">
<CardContent className="p-6 md:p-8">
<div className="flex flex-col md:flex-row gap-6">
{story.imageUrl && (
<div className="w-full md:w-48 h-48 rounded-xl overflow-hidden shrink-0">
<img
src={story.imageUrl}
alt={`${getDisplayName(story)}'s story`}
className="w-full h-full object-cover"
/>
</div>
)}
<div className="flex-1">
<div className="relative mb-4">
<Quote className="absolute -top-2 -left-2 h-8 w-8 text-emerald-200" />
<p className="text-lg md:text-xl italic pl-8">
“{story.quote || story.story.slice(0, 150)}...”
</p>
</div>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-emerald-100 flex items-center justify-center text-emerald-700 font-medium">
{getInitials(story)}
</div>
<div>
<p className="font-semibold">{getDisplayName(story)}</p>
{story.programName && (
<p className="text-sm text-stone-500">{story.programName}</p>
)}
</div>
</div>
<div className="flex flex-wrap gap-4 mb-4">
{story.benefitReceivedCents && (
<Badge className="bg-emerald-100 text-emerald-700">
{formatBenefit(story.benefitReceivedCents)} in benefits
</Badge>
)}
<Badge variant="secondary">
<ThumbsUp className="h-3 w-3 mr-1" />
{story.helpfulCount} found helpful
</Badge>
</div>
<Link
href={`/stories/${story.id}`}
className="inline-flex items-center gap-1 text-sm font-medium text-emerald-600"
>
Read full story
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
</CardContent>
</Card>
);
}
// Default variant
return (
<Card className="h-full hover:shadow-md transition-shadow">
<CardContent className="p-6">
{/* Header */}
<div className="flex items-start justify-between gap-4 mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-stone-200 flex items-center justify-center">
{story.isAnonymous ? (
<User className="h-5 w-5" />
) : (
getInitials(story)
)}
</div>
<div>
<p className="font-medium">{getDisplayName(story)}</p>
<p className="text-xs text-stone-500">
{formatDate(story.createdAt)}
</p>
</div>
</div>
{story.programName && (
<Badge variant="secondary">{story.programName}</Badge>
)}
</div>
{/* Title & Content */}
<h3 className="font-semibold mb-2">{story.title}</h3>
{story.quote ? (
<div className="relative mb-4">
<Quote className="absolute -top-1 -left-1 h-5 w-5 text-stone-200" />
<p className="text-stone-600 italic pl-5 line-clamp-3">
“{story.quote}”
</p>
</div>
) : (
<p className="text-stone-600 line-clamp-3 mb-4">{story.story}</p>
)}
{/* Benefit */}
{story.benefitReceivedCents && (
<div className="flex items-center gap-2 mb-4 text-emerald-700 bg-emerald-50 rounded-lg px-3 py-2">
<DollarSign className="h-4 w-4" />
<span className="text-sm font-medium">
Received {formatBenefit(story.benefitReceivedCents)} in benefits
</span>
</div>
)}
{/* Footer */}
<div className="flex items-center justify-between pt-4 border-t">
<Button
variant={hasVoted ? "secondary" : "outline"}
size="sm"
onClick={() => onMarkHelpful?.(story.id)}
disabled={hasVoted}
>
<ThumbsUp className={cn("h-4 w-4 mr-1.5", hasVoted && "fill-current")} />
{hasVoted ? "Thanks!" : "Helpful"} ({story.helpfulCount})
</Button>
<Link
href={`/stories/${story.id}`}
className="text-sm font-medium text-stone-600 hover:text-stone-900"
>
Read more
</Link>
</div>
</CardContent>
</Card>
);
}
Stories Carousel
Horizontal scrolling carousel for program pages.// components/success-stories/program-stories.tsx
export function ProgramStories({
stories,
programName,
title,
showViewAll = true,
viewAllHref = "/stories",
onMarkHelpful,
}: ProgramStoriesProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const checkScroll = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
setCanScrollLeft(container.scrollLeft > 0);
setCanScrollRight(
container.scrollLeft < container.scrollWidth - container.clientWidth - 10
);
}, []);
const scroll = (direction: "left" | "right") => {
const container = scrollContainerRef.current;
if (!container) return;
container.scrollBy({
left: direction === "left" ? -300 : 300,
behavior: "smooth",
});
};
if (stories.length === 0) return null;
return (
<section className="py-8">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<Sparkles className="h-5 w-5 text-amber-500" />
{title || `Success Stories from ${programName}`}
</h2>
<p className="text-stone-500 mt-1">
Real experiences from people who received benefits
</p>
</div>
{showViewAll && (
<Link href={viewAllHref}>
<Button variant="outline">View All Stories</Button>
</Link>
)}
</div>
<div className="relative">
{/* Scroll buttons */}
{canScrollLeft && (
<button
onClick={() => scroll("left")}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 w-10 h-10 bg-white border rounded-full shadow-md flex items-center justify-center -ml-5"
>
<ChevronLeft className="h-5 w-5" />
</button>
)}
{canScrollRight && (
<button
onClick={() => scroll("right")}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 w-10 h-10 bg-white border rounded-full shadow-md flex items-center justify-center -mr-5"
>
<ChevronRight className="h-5 w-5" />
</button>
)}
{/* Stories container */}
<div
ref={scrollContainerRef}
onScroll={checkScroll}
className="flex gap-4 overflow-x-auto scrollbar-hide pb-4 -mx-4 px-4"
>
{stories.map(story => (
<div key={story.id} className="flex-shrink-0 w-72 md:w-80">
<StoryCard
story={story}
variant="compact"
onMarkHelpful={onMarkHelpful}
/>
</div>
))}
</div>
</div>
</section>
);
}
Moderation Workflow
Stories go through moderation before publishing.Moderation Functions
// lib/dal/repositories/success-stories.repository.ts
export async function approveStory(
storyId: string,
moderatorId: string
): Promise<void> {
await supabaseAdmin
.from("success_stories")
.update({
status: "approved",
moderated_by: moderatorId,
moderated_at: new Date().toISOString(),
})
.eq("id", storyId);
}
export async function rejectStory(
storyId: string,
moderatorId: string,
reason: string
): Promise<void> {
await supabaseAdmin
.from("success_stories")
.update({
status: "rejected",
moderated_by: moderatorId,
moderated_at: new Date().toISOString(),
rejection_reason: reason,
})
.eq("id", storyId);
}
export async function featureStory(
storyId: string,
moderatorId: string
): Promise<void> {
await supabaseAdmin
.from("success_stories")
.update({
status: "featured",
moderated_by: moderatorId,
moderated_at: new Date().toISOString(),
})
.eq("id", storyId);
}
Helpful Voting
Deduplicated voting with database function.CREATE OR REPLACE FUNCTION mark_story_helpful(
p_story_id UUID,
p_user_id TEXT DEFAULT NULL,
p_session_id TEXT DEFAULT NULL
)
RETURNS BOOLEAN AS $$
DECLARE
v_inserted BOOLEAN;
BEGIN
-- Try to insert vote (will fail silently if duplicate)
INSERT INTO story_helpful_votes (story_id, user_id, session_id)
VALUES (p_story_id, p_user_id, p_session_id)
ON CONFLICT DO NOTHING;
GET DIAGNOSTICS v_inserted = ROW_COUNT;
IF v_inserted THEN
-- Increment helpful count
UPDATE success_stories
SET helpful_count = helpful_count + 1
WHERE id = p_story_id
AND status IN ('approved', 'featured');
END IF;
RETURN v_inserted > 0;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Next Steps
Notifications
Notify users about success stories
Case Manager
Share client success stories