Scheduled Reports API
Developer guide for implementing and extending scheduled reports functionality.
Architecture Overview
The scheduled reports system consists of four main layers:
1. Database Layer
report_templates- Pre-built templatesscheduled_reports- Report configurationsreport_executions- Execution history
2. Service Layer
ReportDataAggregator- Data collectionReportGenerator- HTML generationEmailService- Email deliveryScheduledReportsService- Orchestration
3. API Layer
- Server Actions (
app/actions/scheduled-reports.ts) - Cron Endpoint (
app/api/cron/reports/route.ts)
4. UI Layer
- List page (
app/dashboard/settings/reports/page.tsx) - Form component (
components/scheduled-reports/new-report-form.tsx)
Server Actions
All client-to-server communication uses Next.js Server Actions.
Creating a Report
import { createScheduledReport } from '@/app/actions/scheduled-reports';
const result = await createScheduledReport({
orgId: 'org-uuid',
createdBy: 'user-uuid',
name: 'Weekly Summary',
description: 'Executive overview',
templateType: 'executive_summary',
dateRangeType: 'last_7_days',
frequency: 'weekly',
dayOfWeek: 1, // Monday
timeOfDay: '09:00',
timezone: 'America/New_York',
format: 'html',
recipients: ['user@example.com'],
ccRecipients: ['manager@example.com'],
includeCsv: false,
});
if (result.success) {
console.log('Report created:', result.report);
} else {
console.error('Error:', result.error);
}
Listing Reports
import { getScheduledReports } from '@/app/actions/scheduled-reports';
const result = await getScheduledReports(orgId);
if (result.success) {
result.reports?.forEach(report => {
console.log(report.name, report.status);
});
}
Executing a Report
import { executeReportNow } from '@/app/actions/scheduled-reports';
const result = await executeReportNow(reportId, true); // true = send email
if (result.success) {
console.log('Execution:', result.execution);
console.log('Status:', result.execution?.status);
console.log('Recipients sent:', result.execution?.recipients_sent);
}
Updating a Report
import { updateScheduledReport } from '@/app/actions/scheduled-reports';
const result = await updateScheduledReport(reportId, {
name: 'Updated Name',
recipients: ['new@example.com'],
is_active: false, // pause
});
Deleting a Report
import { deleteScheduledReport } from '@/app/actions/scheduled-reports';
const result = await deleteScheduledReport(reportId);
Service Layer
ReportDataAggregator
Collects and aggregates analytics data.
import { ReportDataAggregator } from '@/lib/services/report-data-aggregator';
const aggregator = new ReportDataAggregator(
orgId,
{
start_date: '2025-01-01',
end_date: '2025-01-31',
}
);
// Get summary statistics
const stats = await aggregator.getSummaryStats();
console.log('Total queries:', stats.metrics[0].value);
// Get trend data
const trends = await aggregator.getTrendChart();
console.log('Data points:', trends.chart.labels.length);
// Get top performers
const topQueries = await aggregator.getTopPerformers(10);
console.log('Top queries:', topQueries.items);
// Generate complete report data
const reportData = await aggregator.generateReportData([
'summary_stats',
'trend_chart',
'top_performers',
'recommendations'
]);
ReportGenerator
Generates HTML reports from aggregated data.
import { ReportGenerator } from '@/lib/services/report-generator';
const generator = new ReportGenerator(orgId);
const report = await generator.generateReport(
'executive_summary',
'last_30_days'
);
console.log('HTML length:', report.html?.length);
console.log('Data:', report.data);
EmailService
Sends reports via email.
import { emailService } from '@/lib/services/email-service';
// Check if configured
if (emailService.isConfigured()) {
// Send report
const result = await emailService.sendReport({
to: ['recipient@example.com'],
cc: ['manager@example.com'],
subject: 'Weekly Report - Jan 2025',
html_content: '<html>...</html>',
pdf_attachment: {
filename: 'report.pdf',
url: 'https://storage.example.com/report.pdf',
},
});
console.log('Sent to:', result.sent_to);
console.log('Failed:', result.failed_to);
}
// Send test email
const testResult = await emailService.sendTestEmail('test@example.com');
ScheduledReportsService
Main orchestration service.
import { scheduledReportsService } from '@/lib/services/scheduled-reports';
// Get templates
const templates = await scheduledReportsService.getReportTemplates();
// Execute report
const execution = await scheduledReportsService.executeReportNow(
reportId,
true // sendEmail
);
Database Schema
report_templates
CREATE TABLE report_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
sections JSONB NOT NULL,
default_config JSONB DEFAULT '{}',
is_system BOOLEAN DEFAULT false,
is_public BOOLEAN DEFAULT true,
created_by UUID REFERENCES auth.users(id),
org_id UUID REFERENCES organizations(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
scheduled_reports
CREATE TABLE scheduled_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id),
created_by UUID NOT NULL REFERENCES auth.users(id),
name TEXT NOT NULL,
description TEXT,
template_id UUID REFERENCES report_templates(id),
template_type TEXT NOT NULL,
date_range_type TEXT NOT NULL,
frequency TEXT NOT NULL,
day_of_week INTEGER,
day_of_month INTEGER,
time_of_day TIME DEFAULT '09:00:00',
timezone TEXT DEFAULT 'UTC',
format TEXT DEFAULT 'html',
recipients TEXT[] NOT NULL,
cc_recipients TEXT[] DEFAULT '{}',
include_csv BOOLEAN DEFAULT false,
config JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT true,
last_sent_at TIMESTAMP WITH TIME ZONE,
next_scheduled_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
report_executions
CREATE TABLE report_executions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
report_id UUID NOT NULL REFERENCES scheduled_reports(id),
executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
status TEXT NOT NULL,
report_data JSONB,
html_content TEXT,
pdf_url TEXT,
csv_url TEXT,
recipients_sent TEXT[],
recipients_failed TEXT[],
email_subject TEXT,
error_message TEXT,
error_details JSONB,
generation_time_ms INTEGER,
delivery_time_ms INTEGER,
total_time_ms INTEGER,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Cron Job
The automated scheduler runs every hour via Vercel Cron.
Endpoint
// app/api/cron/reports/route.ts
export async function GET(request: Request) {
// Verify CRON_SECRET
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Find due reports
const { data: dueReports } = await supabase
.from('scheduled_reports')
.select('*')
.eq('is_active', true)
.lte('next_scheduled_at', now.toISOString());
// Execute each report
for (const report of dueReports) {
await executeReport(report);
}
}
Configuration
// vercel.json
{
"crons": [{
"path": "/api/cron/reports",
"schedule": "0 * * * *"
}]
}
Testing Locally
# Development test endpoint (no auth required)
curl http://localhost:3000/api/cron/reports/test
# Production-like test (requires CRON_SECRET)
curl -H "Authorization: Bearer your-secret" \
http://localhost:3000/api/cron/reports
Type Definitions
All types are defined in lib/types/scheduled-reports.ts.
Key Types
export type ReportFrequency = 'daily' | 'weekly' | 'monthly';
export type ReportTemplateType =
| 'executive_summary'
| 'detailed_analytics'
| 'competitive_intel'
| 'content_performance'
| 'cost_analysis'
| 'custom';
export type ReportDateRangeType =
| 'last_7_days'
| 'last_30_days'
| 'last_week'
| 'last_month'
| 'month_to_date'
| 'quarter_to_date'
| 'year_to_date'
| 'custom';
export type ReportExecutionStatus =
| 'pending'
| 'generating'
| 'sending'
| 'success'
| 'failed'
| 'partial';
export interface GeneratedReportData {
generated_at: string;
date_range: {
start_date: string;
end_date: string;
label: string;
};
organization: {
id: string;
name: string;
};
sections: {
type: string;
title: string;
data: unknown;
}[];
summary: {
total_queries: number;
total_citations: number;
avg_citation_rate: number;
cost_total: number;
};
}
Extending the System
Adding a New Template
- Create template in database:
INSERT INTO report_templates (name, description, category, sections, is_system)
VALUES (
'Custom Template',
'My custom template',
'analytics',
'[
{"type": "summary_stats", "title": "Overview"},
{"type": "custom_section", "title": "Custom Data"}
]'::jsonb,
false
);
- Add section renderer in
report-generator.ts:
function renderCustomSection(data: CustomSectionData): string {
return `
<div class="section">
<h2 class="section-title">${data.title}</h2>
<div class="content">
${data.content}
</div>
</div>
`;
}
- Update generateHTML switch:
case 'custom_section':
html += renderCustomSection(section.data as CustomSectionData);
break;
Adding a New Data Source
- Add method to ReportDataAggregator:
async getCustomData(): Promise<CustomData> {
const supabase = await createClient();
const { data } = await supabase
.from('custom_table')
.select('*')
.gte('created_at', this.dateRange.start_date)
.lte('created_at', this.dateRange.end_date);
return { items: data || [] };
}
- Update generateReportData:
case 'custom_data':
data = await this.getCustomData();
break;
Environment Variables
# Required for email delivery
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxx
# Required for cron authentication
CRON_SECRET=random_secret_here
# Optional
RESEND_FROM_EMAIL=reports@yourdomain.com
RESEND_REPLY_TO_EMAIL=noreply@yourdomain.com
Error Handling
All service methods return a standardized response:
{
success: boolean;
data?: T;
error?: string;
}
Example error handling:
const result = await createScheduledReport(params);
if (!result.success) {
// Handle error
console.error('Failed:', result.error);
toast.error(result.error);
return;
}
// Success
console.log('Created:', result.report);
Performance Considerations
Report Generation
- Average: 2-5 seconds
- Large reports (>100 queries): 5-10 seconds
- Use database indexes on timestamp columns
- Aggregate data efficiently
Email Delivery
- 1-3 seconds per recipient
- Send to recipients individually for tracking
- Handle failures gracefully
Cron Execution
- 10-minute timeout on Vercel
- Process reports sequentially
- Log all executions
- Implement retry logic for failures
Testing
Unit Tests
// __tests__/services/report-generator.test.ts
import { ReportGenerator } from '@/lib/services/report-generator';
describe('ReportGenerator', () => {
it('generates HTML report', async () => {
const generator = new ReportGenerator('org-id');
const report = await generator.generateReport(
'executive_summary',
'last_7_days'
);
expect(report.html).toBeDefined();
expect(report.data.sections).toHaveLength(5);
});
});
Integration Tests
// __tests__/api/cron/reports.test.ts
import { GET } from '@/app/api/cron/reports/route';
describe('Cron Endpoint', () => {
it('requires authentication', async () => {
const request = new Request('http://localhost/api/cron/reports');
const response = await GET(request);
expect(response.status).toBe(401);
});
});
Related Documentation
- User Guide: Scheduled Reports
- Vercel Deployment - See installation guide for deployment instructions