/
Documentation

Tutorial: Smart Inventory Alerts (Next.js)

Build a Next.js add-on that monitors stock levels, shows low-stock products, predicts reorder dates, and sends push notifications — all in real-time.

What you'll learn

• Building a multi-page Next.js add-on with App Router
• Using useProducts and useInventory hooks with filters
• Creating per-org configuration (alert thresholds)
• Subscribing to webhook events (product.low_stock)
• Sending push notifications to users
• API route handlers for webhook reception

Final result:

An add-on with a stock alert dashboard, configurable thresholds per product, webhook-driven real-time updates, and automatic notification when products need reordering.

Difficulty: IntermediateTime: 40 minutesFramework: Next.js 14+Scopes: products.read, inventory.read, notifications.write

Step 1: Project Setup

Terminal
npx create-next-app@latest smart-inventory-alerts --typescript --tailwind --app --src-dir
cd smart-inventory-alerts
npm install @syncbooks/addon-sdk

Step 2: App Layout with SDK Provider

src/app/layout.tsx
import './globals.css'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className="bg-gray-50 text-gray-900">{children}</body>
    </html>
  )
}
src/app/providers.tsx
"use client"

import { SyncBooksProvider } from '@syncbooks/addon-sdk'

export default function Providers({ children }: { children: React.ReactNode }) {
  return <SyncBooksProvider>{children}</SyncBooksProvider>
}
src/app/page.tsx
import Providers from './providers'
import InventoryAlerts from './components/inventory-alerts'

export default function Home() {
  return (
    <Providers>
      <InventoryAlerts />
    </Providers>
  )
}

Step 3: The Inventory Alerts Component

src/app/components/inventory-alerts.tsx
"use client"

import { useSyncBooks } from '@syncbooks/addon-sdk'
import { useState, useEffect } from 'react'

interface Product {
  _id: string
  name: string
  sku: string
  currentStock: number
  reorderLevel: number
  unit: string
  sellingPrice: number
  costPrice: number
}

export default function InventoryAlerts() {
  const { ready, api, notify } = useSyncBooks()
  const [products, setProducts] = useState<Product[]>([])
  const [loading, setLoading] = useState(true)
  const [filter, setFilter] = useState<'all' | 'low' | 'out'>('low')
  const [config, setConfig] = useState<any>({})

  // Fetch inventory data
  useEffect(() => {
    if (!ready || !api) return
    fetchProducts()
    loadConfig()
  }, [ready, api])

  async function fetchProducts() {
    setLoading(true)
    try {
      const res = await api!.get('/inventory', { params: { lowStock: filter === 'low' ? 'true' : undefined, limit: 50 } })
      setProducts(res.data || [])
    } catch (err) {
      console.error('Failed to fetch inventory:', err)
    }
    setLoading(false)
  }

  async function loadConfig() {
    try {
      const res = await api!.get('/config')
      setConfig(res.data || {})
    } catch {}
  }

  async function saveThreshold(productId: string, threshold: number) {
    const newConfig = { ...config, thresholds: { ...(config.thresholds || {}), [productId]: threshold } }
    await api!.put('/config', { config: newConfig })
    setConfig(newConfig)
    notify?.({ type: 'success', title: 'Threshold saved', message: `Alert threshold updated to ${threshold} units` })
  }

  async function sendReorderNotification(product: Product) {
    try {
      await api!.post('/notifications', {
        title: '📦 Reorder Alert',
        message: `${product.name} (SKU: ${product.sku}) is at ${product.currentStock} units. Reorder level: ${product.reorderLevel}. Time to restock!`,
        type: 'warning',
      })
      notify?.({ type: 'success', title: 'Notification sent', message: `Reorder alert sent for ${product.name}` })
    } catch (err) {
      notify?.({ type: 'error', title: 'Failed', message: 'Could not send notification' })
    }
  }

  useEffect(() => { if (ready) fetchProducts() }, [filter])

  if (!ready || loading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="w-8 h-8 border-4 border-amber-500 border-t-transparent rounded-full animate-spin" />
      </div>
    )
  }

  const lowStockProducts = products.filter(p => p.currentStock <= p.reorderLevel)
  const outOfStock = products.filter(p => p.currentStock === 0)
  const healthyStock = products.filter(p => p.currentStock > p.reorderLevel)

  return (
    <div className="p-4 space-y-5 max-w-3xl mx-auto">
      <div>
        <h1 className="text-xl font-bold">📦 Inventory Alerts</h1>
        <p className="text-sm text-gray-500">Monitor stock levels and get notified before you run out</p>
      </div>

      {/* Stats */}
      <div className="grid grid-cols-3 gap-3">
        <button onClick={() => setFilter('low')} className={`p-3 rounded-lg border text-center transition ${filter === 'low' ? 'bg-amber-50 border-amber-300' : 'hover:bg-gray-50'}`}>
          <p className="text-2xl font-bold text-amber-600">{lowStockProducts.length}</p>
          <p className="text-xs text-gray-500">Low Stock</p>
        </button>
        <button onClick={() => setFilter('out')} className={`p-3 rounded-lg border text-center transition ${filter === 'out' ? 'bg-red-50 border-red-300' : 'hover:bg-gray-50'}`}>
          <p className="text-2xl font-bold text-red-600">{outOfStock.length}</p>
          <p className="text-xs text-gray-500">Out of Stock</p>
        </button>
        <button onClick={() => setFilter('all')} className={`p-3 rounded-lg border text-center transition ${filter === 'all' ? 'bg-green-50 border-green-300' : 'hover:bg-gray-50'}`}>
          <p className="text-2xl font-bold text-green-600">{healthyStock.length}</p>
          <p className="text-xs text-gray-500">Healthy</p>
        </button>
      </div>

      {/* Product List */}
      <div className="space-y-2">
        {products.length === 0 ? (
          <div className="text-center py-8 text-gray-400">
            <p className="font-medium">No products found</p>
            <p className="text-sm">Products with inventory tracking will appear here</p>
          </div>
        ) : (
          products.map(product => {
            const stockPercent = product.reorderLevel > 0
              ? Math.min(100, (product.currentStock / (product.reorderLevel * 2)) * 100)
              : 100
            const isLow = product.currentStock <= product.reorderLevel
            const isOut = product.currentStock === 0

            return (
              <div key={product._id} className={`border rounded-lg p-3 ${isOut ? 'border-red-200 bg-red-50/50' : isLow ? 'border-amber-200 bg-amber-50/50' : 'border-gray-200'}`}>
                <div className="flex items-center justify-between">
                  <div>
                    <p className="font-medium text-sm">{product.name}</p>
                    <p className="text-xs text-gray-500">SKU: {product.sku} · Reorder at: {product.reorderLevel} {product.unit}</p>
                  </div>
                  <div className="text-right">
                    <p className={`text-lg font-bold ${isOut ? 'text-red-600' : isLow ? 'text-amber-600' : 'text-green-600'}`}>
                      {product.currentStock}
                    </p>
                    <p className="text-xs text-gray-500">{product.unit}</p>
                  </div>
                </div>

                {/* Stock Bar */}
                <div className="mt-2 h-2 bg-gray-200 rounded-full overflow-hidden">
                  <div
                    className={`h-full rounded-full transition-all ${isOut ? 'bg-red-500' : isLow ? 'bg-amber-500' : 'bg-green-500'}`}
                    style={{ width: `${stockPercent}%` }}
                  />
                </div>

                {/* Actions */}
                {isLow && (
                  <div className="mt-2 flex gap-2">
                    <button
                      onClick={() => sendReorderNotification(product)}
                      className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded hover:bg-amber-200 transition"
                    >
                      🔔 Send Reorder Alert
                    </button>
                  </div>
                )}
              </div>
            )
          })
        )}
      </div>
    </div>
  )
}

Step 4: Webhook Handler for Real-Time Alerts

Create an API route to receive webhook events from SyncBooks when stock drops:

src/app/api/webhooks/syncbooks/route.ts
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'

// Your app's signing key (set during marketplace registration)
const SIGNING_KEY = process.env.SYNCBOOKS_WEBHOOK_SECRET || ''

function verifySignature(body: string, signature: string): boolean {
  const expected = 'sha256=' + crypto.createHmac('sha256', SIGNING_KEY).update(body).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

export async function POST(req: NextRequest) {
  const body = await req.text()
  const signature = req.headers.get('x-syncbooks-signature') || ''

  // Verify webhook authenticity
  if (SIGNING_KEY && !verifySignature(body, signature)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }

  const event = JSON.parse(body)
  console.log(`[Webhook] Received: ${event.event}`, event.data)

  switch (event.event) {
    case 'product.low_stock':
      // Product fell below reorder level — could send Slack alert, email, etc.
      console.log(`⚠️ Low stock: ${event.data.name} — ${event.data.currentStock} remaining`)
      // In production: send to Slack, email, SMS, or store for dashboard
      break

    case 'product.updated':
      // Stock was updated — refresh dashboard for connected clients
      console.log(`📦 Stock updated: ${event.data.name}`)
      break

    default:
      console.log(`Unhandled event: ${event.event}`)
  }

  return NextResponse.json({ received: true })
}

Webhook verification

Always verify the X-SyncBooks-Signature header in production. The signature is an HMAC-SHA256 hash of the request body using your app's signing key. This prevents forged webhook calls.

Step 5: Settings Page (Per-Org Config)

Let users customize alert thresholds. Create a settings route in your addon:

src/app/settings/page.tsx
import Providers from '../providers'
import SettingsForm from './settings-form'

export default function SettingsPage() {
  return (
    <Providers>
      <SettingsForm />
    </Providers>
  )
}
src/app/settings/settings-form.tsx
"use client"

import { useSyncBooks } from '@syncbooks/addon-sdk'
import { useState, useEffect } from 'react'

export default function SettingsForm() {
  const { ready, api, notify } = useSyncBooks()
  const [config, setConfig] = useState({ defaultThreshold: 10, emailAlerts: true, slackWebhook: '' })
  const [saving, setSaving] = useState(false)

  useEffect(() => {
    if (!ready || !api) return
    api.get('/config').then(res => {
      if (res.data) setConfig({ ...config, ...res.data })
    })
  }, [ready])

  async function handleSave() {
    if (!api) return
    setSaving(true)
    try {
      await api.put('/config', { config })
      notify?.({ type: 'success', title: 'Settings saved', message: 'Your alert preferences have been updated' })
    } catch {
      notify?.({ type: 'error', title: 'Error', message: 'Failed to save settings' })
    }
    setSaving(false)
  }

  if (!ready) return <div className="p-4">Loading...</div>

  return (
    <div className="p-4 max-w-md mx-auto space-y-6">
      <div>
        <h1 className="text-xl font-bold">⚙️ Alert Settings</h1>
        <p className="text-sm text-gray-500">Configure your inventory alert preferences</p>
      </div>

      <div className="space-y-4">
        <div>
          <label className="text-sm font-medium">Default Reorder Threshold</label>
          <input
            type="number"
            value={config.defaultThreshold}
            onChange={e => setConfig(c => ({ ...c, defaultThreshold: +e.target.value }))}
            className="mt-1 w-full px-3 py-2 border rounded-lg text-sm"
          />
          <p className="text-xs text-gray-500 mt-1">Alert when stock falls below this number</p>
        </div>

        <div className="flex items-center gap-3">
          <input
            type="checkbox"
            checked={config.emailAlerts}
            onChange={e => setConfig(c => ({ ...c, emailAlerts: e.target.checked }))}
            className="rounded"
          />
          <label className="text-sm">Send email alerts for low stock</label>
        </div>

        <div>
          <label className="text-sm font-medium">Slack Webhook URL (optional)</label>
          <input
            type="url"
            value={config.slackWebhook}
            onChange={e => setConfig(c => ({ ...c, slackWebhook: e.target.value }))}
            placeholder="https://hooks.slack.com/services/..."
            className="mt-1 w-full px-3 py-2 border rounded-lg text-sm"
          />
        </div>

        <button
          onClick={handleSave}
          disabled={saving}
          className="w-full bg-emerald-600 text-white py-2 rounded-lg font-medium hover:bg-emerald-700 disabled:opacity-50 transition"
        >
          {saving ? 'Saving...' : 'Save Settings'}
        </button>
      </div>
    </div>
  )
}

Step 6: Deploy & Register

Terminal
# Push to GitHub and deploy
git add . && git commit -m "Smart Inventory Alerts addon"
npx vercel --prod
1

Deploy to Vercel

Deploy to Vercel — copy the production URL
2

Open Developer Portal

Go to /developers → Marketplace Add-ons → Submit
3

Set Launch URL

Set Launch URL to your Vercel URL
4

Set Webhook URL

Set Webhook URL to https://your-addon.vercel.app/api/webhooks/syncbooks
5

Select Permissions

Select permissions: products.read, inventory.read, notifications.write
6

Select Events

Select events: product.low_stock, product.updated
7

Submit

Submit for review!

What You Built

✅ A production-ready Next.js add-on with:

  • Real-time stock level monitoring with visual progress bars
  • Filterable views (Low Stock / Out of Stock / Healthy)
  • One-click reorder notifications to org users
  • Webhook handler for real-time stock change events
  • Per-org settings page with config storage API
  • HMAC webhook signature verification
  • Responsive design for sidebar and widget placements

Next Steps

  • Add Slack/Teams notifications when stock is critically low
  • Build a "Purchase Order Suggestions" feature that auto-creates POs
  • Add historical stock trend charts
  • Implement ABC analysis (classify products by revenue impact)
  • Add barcode scanning support for quick stock counts