Beginner ~3 hours

Build an AI Donor Insights Dashboard

Create a powerful donor analytics dashboard with AI-powered insights. Track donations, analyze giving trends, predict donor behavior, and generate automated reports - all designed for churches, NGOs, and nonprofit organizations.

What We're Building

In this guide, you'll build a complete donor insights dashboard that helps nonprofits understand their giving patterns and make data-driven decisions. The dashboard will feature:

  • Real-time donation tracking - View incoming donations as they happen
  • Interactive data visualizations - Charts showing donation trends, campaign performance, and donor segments
  • AI-powered insights - Claude analyzes your donor data to identify trends, predict lapsed donors, and suggest engagement strategies
  • Automated report generation - Create PDF reports for board meetings and stakeholders
  • Email summary automation - Weekly digest emails with key metrics and AI recommendations

This dashboard addresses a major nonprofit challenge: 73% of donors prefer online giving, yet many organizations lack the tools to analyze their donor base effectively. By the end of this guide, you'll have a production-ready dashboard that can help increase donor retention and optimize fundraising campaigns.

Prerequisites

Before starting this guide, make sure you have the following:

  • Node.js 18+ installed on your machine
  • A Supabase account (free tier works perfectly)
  • A Claude API key from Anthropic
  • A Vercel account for deployment (free tier)
  • Basic familiarity with React and TypeScript (beginner level is fine)

Tech Stack Specification

Here's the technology stack we'll use for this build:

Layer Technology Why This Choice
Frontend Next.js 14, Recharts Server components for fast loading, Recharts for beautiful donation visualizations
Backend Supabase Edge Functions Simple serverless functions, no server management, generous free tier
Database Supabase (PostgreSQL) Built-in auth, real-time subscriptions, row-level security for donor data
AI/API Claude API Best-in-class reasoning for donor trend analysis and natural language insights
Hosting Vercel Free hosting, automatic deployments, excellent Next.js integration

AI Agent Workflow

Here's how to leverage AI tools throughout this build to maximize productivity:

Project Scaffolding with Claude Code

Use Claude Code to scaffold the entire project structure, including the database schema, API routes, and React components. This saves hours of boilerplate setup.

Prompt
# Example prompt for Claude Code
Create a Next.js 14 donor dashboard with:
- Supabase integration for donor/donation data
- Dashboard page with donation charts using Recharts
- API route for Claude AI insights generation
- TypeScript throughout
- Tailwind CSS styling

UI Generation with v0.dev

Use v0.dev to generate the dashboard UI components. Describe the nonprofit-focused interface you need, and it will create production-ready React components with Tailwind styling.

Pro Tip

When prompting v0.dev, mention "nonprofit dashboard" and "donation metrics" to get more relevant card layouts and color schemes. Request dark mode support since many dashboard users prefer it.

Development with Cursor

Cursor's AI-powered autocomplete understands your Supabase schema and can generate type-safe queries. Use Cmd+K to ask questions about integrating Claude API or fixing TypeScript errors.

Step-by-Step Build Guide

Phase 1: Dashboard Layout Setup

Start by creating the Next.js project and setting up the basic dashboard structure with navigation, sidebar, and main content area.

Terminal
# Create new Next.js project
npx create-next-app@latest donor-dashboard --typescript --tailwind --eslint --app

# Navigate to project
cd donor-dashboard

# Install dependencies
npm install @supabase/supabase-js recharts @anthropic-ai/sdk date-fns

# Install dev dependencies
npm install -D @types/recharts

Create the dashboard layout component that will wrap all dashboard pages:

TypeScript - app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="flex min-h-screen bg-gray-950">
      {/* Sidebar */}
      <aside className="w-64 border-r border-gray-800 p-6">
        <div className="mb-8">
          <h1 className="text-xl font-bold text-white">
            Donor Insights
          </h1>
          <p className="text-sm text-gray-400">AI-Powered Analytics</p>
        </div>
        <nav className="space-y-2">
          <NavLink href="/dashboard" icon="chart">Overview</NavLink>
          <NavLink href="/dashboard/donors" icon="users">Donors</NavLink>
          <NavLink href="/dashboard/campaigns" icon="target">Campaigns</NavLink>
          <NavLink href="/dashboard/insights" icon="sparkles">AI Insights</NavLink>
          <NavLink href="/dashboard/reports" icon="file">Reports</NavLink>
        </nav>
      </aside>

      {/* Main content */}
      <main className="flex-1 p-8">
        {children}
      </main>
    </div>
  )
}

Phase 2: Donor Database Schema

Set up the Supabase database with tables for donors, donations, and campaigns. This schema supports tracking donation history, recurring gifts, and campaign attribution.

SQL - Supabase SQL Editor
-- Donors table
CREATE TABLE donors (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  first_name TEXT NOT NULL,
  last_name TEXT NOT NULL,
  phone TEXT,
  address JSONB,
  first_donation_date TIMESTAMPTZ,
  last_donation_date TIMESTAMPTZ,
  total_donated DECIMAL(10,2) DEFAULT 0,
  donation_count INTEGER DEFAULT 0,
  is_recurring BOOLEAN DEFAULT false,
  donor_tier TEXT DEFAULT 'standard', -- standard, silver, gold, platinum
  tags TEXT[],
  notes TEXT,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- Campaigns table
CREATE TABLE campaigns (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT,
  goal_amount DECIMAL(10,2),
  raised_amount DECIMAL(10,2) DEFAULT 0,
  start_date TIMESTAMPTZ NOT NULL,
  end_date TIMESTAMPTZ,
  status TEXT DEFAULT 'active', -- active, completed, paused
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Donations table
CREATE TABLE donations (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  donor_id UUID REFERENCES donors(id) ON DELETE SET NULL,
  campaign_id UUID REFERENCES campaigns(id) ON DELETE SET NULL,
  amount DECIMAL(10,2) NOT NULL,
  currency TEXT DEFAULT 'USD',
  payment_method TEXT, -- card, bank, check, cash
  is_recurring BOOLEAN DEFAULT false,
  recurring_frequency TEXT, -- weekly, monthly, yearly
  status TEXT DEFAULT 'completed', -- pending, completed, refunded
  notes TEXT,
  donated_at TIMESTAMPTZ DEFAULT now(),
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Create indexes for performance
CREATE INDEX idx_donations_donor_id ON donations(donor_id);
CREATE INDEX idx_donations_campaign_id ON donations(campaign_id);
CREATE INDEX idx_donations_donated_at ON donations(donated_at);
CREATE INDEX idx_donors_last_donation ON donors(last_donation_date);

Phase 3: Data Visualization Components

Build the chart components using Recharts to display donation trends, campaign progress, and donor segments. These visualizations help nonprofit staff quickly understand their fundraising performance.

TypeScript - components/DonationTrendChart.tsx
'use client'

import {
  AreaChart, Area, XAxis, YAxis, CartesianGrid,
  Tooltip, ResponsiveContainer
} from 'recharts'
import { format } from 'date-fns'

interface DonationData {
  date: string
  amount: number
  count: number
}

interface Props {
  data: DonationData[]
  timeRange: '7d' | '30d' | '90d' | '1y'
}

export function DonationTrendChart({ data, timeRange }: Props) {
  const formatDate = (dateStr: string) => {
    const date = new Date(dateStr)
    return timeRange === '7d'
      ? format(date, 'EEE')
      : format(date, 'MMM d')
  }

  const formatCurrency = (value: number) =>
    new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 0,
    }).format(value)

  return (
    <div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
      <div className="flex justify-between items-center mb-6">
        <div>
          <h3 className="text-lg font-semibold text-white">
            Donation Trends
          </h3>
          <p className="text-sm text-gray-400">
            Total donations over time
          </p>
        </div>
        <TimeRangeSelector value={timeRange} />
      </div>

      <ResponsiveContainer width="100%" height={300}>
        <AreaChart data={data}>
          <defs>
            <linearGradient id="donationGradient" x1="0" y1="0" x2="0" y2="1">
              <stop offset="5%" stopColor="#8b5cf6" stopOpacity={0.3}/>
              <stop offset="95%" stopColor="#8b5cf6" stopOpacity={0}/>
            </linearGradient>
          </defs>
          <CartesianGrid strokeDasharray="3 3" stroke="#374151" />
          <XAxis
            dataKey="date"
            tickFormatter={formatDate}
            stroke="#9ca3af"
            fontSize={12}
          />
          <YAxis
            tickFormatter={formatCurrency}
            stroke="#9ca3af"
            fontSize={12}
          />
          <Tooltip
            content={<CustomTooltip />}
            cursor={{ stroke: '#8b5cf6', strokeWidth: 1 }}
          />
          <Area
            type="monotone"
            dataKey="amount"
            stroke="#8b5cf6"
            strokeWidth={2}
            fill="url(#donationGradient)"
          />
        </AreaChart>
      </ResponsiveContainer>
    </div>
  )
}

Phase 4: AI Insight Generation

This is the core of the dashboard - using Claude to analyze donor data and generate actionable insights. The AI identifies patterns humans might miss, such as donors at risk of lapsing or optimal times for outreach.

TypeScript - app/api/insights/route.ts
import Anthropic from '@anthropic-ai/sdk'
import { createClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
})

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
)

export async function POST(request: Request) {
  try {
    const { insightType } = await request.json()

    // Fetch donor data for analysis
    const { data: donors } = await supabase
      .from('donors')
      .select(`
        *,
        donations (
          amount,
          donated_at,
          campaign_id,
          is_recurring
        )
      `)
      .order('total_donated', { ascending: false })
      .limit(500)

    // Fetch recent donation trends
    const { data: recentDonations } = await supabase
      .from('donations')
      .select('amount, donated_at, is_recurring')
      .gte('donated_at', new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString())
      .order('donated_at', { ascending: false })

    // Build context for Claude
    const donorSummary = {
      totalDonors: donors?.length || 0,
      totalRaised: donors?.reduce((sum, d) => sum + (d.total_donated || 0), 0),
      recurringDonors: donors?.filter(d => d.is_recurring).length || 0,
      avgDonation: recentDonations?.length
        ? recentDonations.reduce((sum, d) => sum + d.amount, 0) / recentDonations.length
        : 0,
      // Donors who haven't given in 60+ days
      atRiskDonors: donors?.filter(d => {
        if (!d.last_donation_date) return false
        const daysSince = (Date.now() - new Date(d.last_donation_date).getTime()) / (1000 * 60 * 60 * 24)
        return daysSince > 60 && d.donation_count > 1
      }).length || 0,
    }

    // Generate insights with Claude
    const message = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      messages: [{
        role: 'user',
        content: `You are an AI analyst for a nonprofit donor management system.
Analyze this donor data and provide actionable insights.

Donor Summary:
- Total Donors: ${donorSummary.totalDonors}
- Total Raised (all time): $${donorSummary.totalRaised.toLocaleString()}
- Recurring Donors: ${donorSummary.recurringDonors}
- Average Recent Donation: $${donorSummary.avgDonation.toFixed(2)}
- At-Risk Donors (60+ days inactive): ${donorSummary.atRiskDonors}

Recent Donation Data (last 90 days):
${JSON.stringify(recentDonations?.slice(0, 50), null, 2)}

Top Donor Profiles:
${JSON.stringify(donors?.slice(0, 20).map(d => ({
  totalDonated: d.total_donated,
  donationCount: d.donation_count,
  isRecurring: d.is_recurring,
  tier: d.donor_tier,
  daysSinceLastDonation: d.last_donation_date
    ? Math.floor((Date.now() - new Date(d.last_donation_date).getTime()) / (1000 * 60 * 60 * 24))
    : null
})), null, 2)}

Insight Type Requested: ${insightType}

Based on this data, provide:
1. Key Insight: One paragraph summary of the most important finding
2. Trend Analysis: What patterns do you see in donation behavior?
3. At-Risk Alert: Specific recommendations for re-engaging lapsed donors
4. Growth Opportunity: One actionable suggestion to increase donations
5. Predicted Outcome: What might happen in the next 30 days based on trends?

Format your response as JSON with these exact keys:
{
  "keyInsight": "...",
  "trendAnalysis": "...",
  "atRiskAlert": "...",
  "growthOpportunity": "...",
  "predictedOutcome": "...",
  "confidenceScore": 0.0-1.0
}`
      }]
    })

    // Parse Claude's response
    const responseText = message.content[0].type === 'text'
      ? message.content[0].text
      : ''

    // Extract JSON from response
    const jsonMatch = responseText.match(/\{[\s\S]*\}/)
    const insights = jsonMatch ? JSON.parse(jsonMatch[0]) : null

    return NextResponse.json({
      success: true,
      insights,
      summary: donorSummary,
      generatedAt: new Date().toISOString()
    })

  } catch (error) {
    console.error('Insight generation error:', error)
    return NextResponse.json(
      { success: false, error: 'Failed to generate insights' },
      { status: 500 }
    )
  }
}
How the AI Insights Work

Claude analyzes donation patterns, identifies at-risk donors (those who haven't given in 60+ days but have a history of giving), and provides personalized re-engagement strategies. The confidence score helps you prioritize which insights to act on first.

Phase 5: Report Generation Feature

Create a report generation feature that compiles donation data, AI insights, and visualizations into a downloadable PDF for board meetings and stakeholder updates.

TypeScript - lib/generateReport.ts
import { jsPDF } from 'jspdf'
import { format } from 'date-fns'

interface ReportData {
  summary: {
    totalDonors: number
    totalRaised: number
    recurringDonors: number
    newDonorsThisMonth: number
  }
  insights: {
    keyInsight: string
    trendAnalysis: string
    growthOpportunity: string
  }
  topDonors: Array<{
    name: string
    totalDonated: number
  }>
}

export async function generateDonorReport(data: ReportData): Promise<Blob> {
  const doc = new jsPDF()
  const pageWidth = doc.internal.pageSize.getWidth()

  // Header
  doc.setFontSize(24)
  doc.setTextColor(139, 92, 246) // Purple accent
  doc.text('Donor Insights Report', pageWidth / 2, 30, { align: 'center' })

  doc.setFontSize(12)
  doc.setTextColor(100)
  doc.text(format(new Date(), 'MMMM d, yyyy'), pageWidth / 2, 40, { align: 'center' })

  // Summary Section
  doc.setFontSize(16)
  doc.setTextColor(0)
  doc.text('Summary', 20, 60)

  doc.setFontSize(11)
  doc.setTextColor(60)
  const summaryLines = [
    `Total Donors: ${data.summary.totalDonors.toLocaleString()}`,
    `Total Raised: $${data.summary.totalRaised.toLocaleString()}`,
    `Recurring Donors: ${data.summary.recurringDonors}`,
    `New Donors This Month: ${data.summary.newDonorsThisMonth}`,
  ]
  summaryLines.forEach((line, i) => {
    doc.text(line, 25, 72 + (i * 8))
  })

  // AI Insights Section
  doc.setFontSize(16)
  doc.setTextColor(0)
  doc.text('AI-Powered Insights', 20, 115)

  doc.setFontSize(11)
  doc.setTextColor(60)

  const insightY = 127
  doc.setFont('helvetica', 'bold')
  doc.text('Key Finding:', 25, insightY)
  doc.setFont('helvetica', 'normal')
  const keyInsightLines = doc.splitTextToSize(data.insights.keyInsight, pageWidth - 50)
  doc.text(keyInsightLines, 25, insightY + 8)

  // Continue with more sections...

  return doc.output('blob')
}

Phase 6: Email Summary Automation

Set up automated weekly email digests that send AI-generated insights to nonprofit staff. This uses Supabase Edge Functions with a cron trigger.

TypeScript - supabase/functions/weekly-digest/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import Anthropic from 'https://esm.sh/@anthropic-ai/sdk'

const anthropic = new Anthropic({
  apiKey: Deno.env.get('ANTHROPIC_API_KEY')!,
})

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // Get last 7 days of donations
  const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)

  const { data: weeklyDonations } = await supabase
    .from('donations')
    .select('amount, donated_at, donor_id')
    .gte('donated_at', weekAgo.toISOString())

  const totalThisWeek = weeklyDonations?.reduce((sum, d) => sum + d.amount, 0) || 0
  const donationCount = weeklyDonations?.length || 0
  const uniqueDonors = new Set(weeklyDonations?.map(d => d.donor_id)).size

  // Generate AI summary
  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 512,
    messages: [{
      role: 'user',
      content: `Write a brief, encouraging weekly donation summary email for nonprofit staff.

This week's stats:
- Total raised: $${totalThisWeek.toLocaleString()}
- Number of donations: ${donationCount}
- Unique donors: ${uniqueDonors}

Write 2-3 short paragraphs highlighting the positive impact. Include one specific
action item for the coming week. Keep the tone warm and mission-focused.`
    }]
  })

  const emailContent = message.content[0].type === 'text'
    ? message.content[0].text
    : ''

  // Send email via your preferred service (Resend, SendGrid, etc.)
  await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'insights@yournonprofit.org',
      to: ['staff@yournonprofit.org'],
      subject: `Weekly Donor Digest: $${totalThisWeek.toLocaleString()} raised!`,
      html: `
        <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
          <h1 style="color: #8b5cf6;">Weekly Donor Insights</h1>
          <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
            <p style="margin: 0; font-size: 24px; font-weight: bold;">$${totalThisWeek.toLocaleString()}</p>
            <p style="margin: 4px 0 0; color: #666;">raised this week from ${uniqueDonors} donors</p>
          </div>
          ${emailContent.split('\n').map(p => `<p>${p}</p>`).join('')}
          <p style="margin-top: 30px; color: #666; font-size: 12px;">
            Generated by AI Donor Insights Dashboard
          </p>
        </div>
      `
    })
  })

  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' }
  })
})
Scheduling the Digest

In Supabase, go to Database > Extensions and enable pg_cron. Then create a cron job: SELECT cron.schedule('weekly-digest', '0 9 * * 1', 'SELECT net.http_post(...)') to run every Monday at 9 AM.

Common Issues & Solutions

Here are some common issues you might encounter and how to solve them:

Claude API Rate Limits

If you hit rate limits during insight generation, implement caching. Store generated insights in Supabase with a timestamp and only regenerate if the cache is older than 1 hour. This also reduces API costs.

Supabase Row Level Security

Remember to configure RLS policies on your donor tables. Use the service role key only in server-side code (API routes, Edge Functions). Never expose it to the client.

Other common issues include:

  • Recharts not rendering - Make sure to add 'use client' directive to chart components
  • TypeScript errors with Supabase - Generate types with supabase gen types typescript
  • Edge Function cold starts - First request may be slow; consider a keep-warm ping

Next Steps

Congratulations on building your AI Donor Insights Dashboard! Here are some ways to extend it:

  • Add Stripe integration - Connect to Stripe webhooks for real-time donation tracking
  • Donor segmentation - Use Claude to automatically categorize donors into engagement segments
  • Predictive giving - Build a model to predict which donors are likely to give during year-end campaigns
  • Mobile app - Create a React Native companion app for on-the-go insights
  • Multi-org support - Add organization-level isolation for SaaS deployment

For more nonprofit technology solutions, check out our Non-Profit Solutions page or browse our other guides for more AI development tutorials.