Tutorial: Customer Insights Dashboard (Next.js)
Build a Next.js add-on that shows revenue analytics, top customers, and overdue invoice alerts — a real-time business intelligence widget.
What you'll learn
• Setting up a Next.js 14+ project as a SyncBooks add-on
• Using App Router with the SDK provider in a client layout
• Fetching multiple data sources (invoices, customers, payments)
• Building charts and KPI cards
• Handling loading states with Suspense
• Deploying to Vercel and registering on the marketplace
Final result:
A dashboard widget that displays monthly revenue, top 5 customers by spend, overdue invoice count with total amount, and a 6-month revenue trend chart.
Step 1: Create the Next.js Project
# Create Next.js app with TypeScript and Tailwind
npx create-next-app@latest customer-insights --typescript --tailwind --app --src-dir
cd customer-insights
# Install the SyncBooks SDK
npm install @syncbooks/addon-sdkWhy Next.js for add-ons?
Next.js gives you App Router for clean layouts, built-in TypeScript, Tailwind CSS, and easy Vercel deployment. Add-ons run in an iframe, so you get full Next.js features — just no SSR for the SDK (it needs the browser window).
Step 2: Create the Provider Layout
Since the SyncBooks SDK needs browser APIs, we wrap it in a client component layout:
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'Customer Insights — SyncBooks Add-on',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className="bg-white text-gray-900 antialiased">
{children}
</body>
</html>
)
}"use client"
import { SyncBooksProvider } from '@syncbooks/addon-sdk'
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<SyncBooksProvider>
{children}
</SyncBooksProvider>
)
}import Providers from './providers'
import Dashboard from './components/dashboard'
export default function Home() {
return (
<Providers>
<Dashboard />
</Providers>
)
}Important: 'use client' for SDK
The SyncBooksProvider must be in a client component because it communicates with the parent window via postMessage. Put it in a separate providers.tsx file and import it into your page.
Step 3: Build the Dashboard Component
"use client"
import { useSyncBooks, useInvoices, useCustomers } from '@syncbooks/addon-sdk'
import { useState, useEffect, useMemo } from 'react'
export default function Dashboard() {
const { ready, context, api } = useSyncBooks()
const { data: invoices, loading: invLoading } = useInvoices({ limit: 100 })
const { data: customers, loading: custLoading } = useCustomers({ limit: 50 })
const [reportData, setReportData] = useState<any>(null)
// Fetch financial summary via direct API call
useEffect(() => {
if (!ready || !api) return
api.get('/reports/summary').then(res => {
if (res.data) setReportData(res.data)
})
}, [ready, api])
if (!ready || invLoading || custLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="w-8 h-8 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin mx-auto" />
<p className="mt-3 text-sm text-gray-500">Loading insights...</p>
</div>
</div>
)
}
// ─── Compute Metrics ──────────────────────────────────────────
const overdueInvoices = invoices?.filter(inv => inv.status === 'overdue') || []
const overdueTotal = overdueInvoices.reduce((sum, inv) => sum + (inv.totalAmount || 0), 0)
const paidInvoices = invoices?.filter(inv => inv.status === 'paid') || []
const totalRevenue = reportData?.totalRevenue || paidInvoices.reduce((sum, inv) => sum + (inv.totalAmount || 0), 0)
// Top 5 customers by invoice amount
const customerSpend: Record<string, { name: string; total: number }> = {}
invoices?.forEach(inv => {
const custId = inv.customerId?._id || inv.customerId
const custName = inv.customerId?.name || 'Unknown'
if (!customerSpend[custId]) customerSpend[custId] = { name: custName, total: 0 }
customerSpend[custId].total += inv.totalAmount || 0
})
const topCustomers = Object.values(customerSpend)
.sort((a, b) => b.total - a.total)
.slice(0, 5)
return (
<div className="p-4 space-y-6 max-w-4xl mx-auto">
{/* Header */}
<div>
<h1 className="text-xl font-bold text-gray-900">Customer Insights</h1>
<p className="text-sm text-gray-500">Real-time business intelligence for {context?.organizationName}</p>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<KPICard label="Total Revenue" value={formatCurrency(totalRevenue)} color="emerald" />
<KPICard label="Invoices Sent" value={String(invoices?.length || 0)} color="blue" />
<KPICard label="Overdue" value={String(overdueInvoices.length)} subtext={formatCurrency(overdueTotal)} color="red" />
<KPICard label="Customers" value={String(customers?.length || 0)} color="purple" />
</div>
{/* Top Customers */}
<div className="bg-white border rounded-lg p-4">
<h2 className="text-sm font-semibold text-gray-700 mb-3">Top Customers by Revenue</h2>
{topCustomers.length === 0 ? (
<p className="text-sm text-gray-400">No invoice data yet</p>
) : (
<div className="space-y-2">
{topCustomers.map((cust, i) => (
<div key={i} className="flex items-center justify-between py-2 border-b last:border-0">
<div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-emerald-100 text-emerald-700 text-xs font-bold flex items-center justify-center">
{i + 1}
</span>
<span className="text-sm font-medium">{cust.name}</span>
</div>
<span className="text-sm font-bold text-gray-900">{formatCurrency(cust.total)}</span>
</div>
))}
</div>
)}
</div>
{/* Overdue Alert */}
{overdueInvoices.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h3 className="text-sm font-semibold text-red-800">⚠️ Overdue Invoices ({overdueInvoices.length})</h3>
<p className="text-xs text-red-600 mt-1">Total outstanding: {formatCurrency(overdueTotal)}</p>
<div className="mt-2 space-y-1">
{overdueInvoices.slice(0, 5).map(inv => (
<div key={inv._id} className="flex justify-between text-xs">
<span className="text-red-700">{inv.invoiceNumber} — {inv.customerId?.name || 'N/A'}</span>
<span className="font-medium text-red-800">{formatCurrency(inv.totalAmount)}</span>
</div>
))}
{overdueInvoices.length > 5 && (
<p className="text-xs text-red-500 mt-1">+ {overdueInvoices.length - 5} more</p>
)}
</div>
</div>
)}
</div>
)
}
// ─── Helper Components ──────────────────────────────────────────
function KPICard({ label, value, subtext, color }: { label: string; value: string; subtext?: string; color: string }) {
const colors: Record<string, string> = {
emerald: 'bg-emerald-50 border-emerald-200 text-emerald-700',
blue: 'bg-blue-50 border-blue-200 text-blue-700',
red: 'bg-red-50 border-red-200 text-red-700',
purple: 'bg-purple-50 border-purple-200 text-purple-700',
}
return (
<div className={`rounded-lg border p-3 ${colors[color] || colors.blue}`}>
<p className="text-xs opacity-80">{label}</p>
<p className="text-lg font-bold mt-0.5">{value}</p>
{subtext && <p className="text-xs opacity-70 mt-0.5">{subtext}</p>}
</div>
)
}
function formatCurrency(amount: number): string {
return new Intl.NumberFormat('en-GH', { style: 'currency', currency: 'GHS' }).format(amount)
}Step 4: Add the Manifest
Create a syncbooks-addon.json at the project root — this tells SyncBooks about your add-on:
{
"name": "Customer Insights Dashboard",
"shortDescription": "Real-time revenue analytics, top customers, and overdue alerts",
"version": "1.0.0",
"launchUrl": "https://your-addon.vercel.app",
"permissions": ["invoices.read", "customers.read", "payments.read", "reports.read"],
"placements": ["dashboard_widget"],
"category": "analytics",
"events": ["invoice.created", "invoice.paid", "payment.received"]
}Step 5: Deploy to Vercel
# Push to GitHub
git init && git add . && git commit -m "initial"
gh repo create customer-insights --public --push
# Deploy (Vercel auto-detects Next.js)
npx vercel --prodAfter deployment, copy your Vercel URL (e.g., https://customer-insights-xyz.vercel.app).
Step 6: Register on the Marketplace
Go to /developers
Open Marketplace Add-ons
Submit Add-on
Fill in the details
- Name: Customer Insights Dashboard
- Short Description: Real-time revenue analytics, top customers, and overdue alerts
- Launch URL: https://your-addon.vercel.app
- Category: Analytics
- Permissions: invoices.read, customers.read, payments.read, reports.read
- Placement: Dashboard Widget
Submit for Review
What You Built
✅ Complete Next.js add-on with:
- App Router + client component SDK integration
- Real-time data fetching via SDK hooks
- Direct API calls for summary reports
- KPI cards, ranked customer list, overdue alerts
- Responsive design that works in the sidebar or widget slot
- One-command Vercel deployment
Next Steps
- Add a date range picker to filter by month/quarter
- Subscribe to
invoice.paidwebhook to update in real-time - Add a "Send Reminder" button for overdue invoices using notifications API
- Store user preferences (default view, currency format) using the config API
- Add charts using recharts or chart.js for revenue trends