# How to Build Security monitoring with V0

- Tool: v0
- Difficulty: Advanced
- Compatibility: V0 Premium or higher
- Last updated: April 2026

## TL;DR

Build a real-time security monitoring dashboard with V0 that tracks login attempts, permission changes, and suspicious activity using Next.js, Supabase, and Recharts. You'll create event ingestion via middleware, live-updating charts, alert rule configuration, and IP blocking — all in about 2-4 hours.

## Before you start

- A V0 account (Premium plan recommended for complex multi-file generation)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Resend account for email notifications (free tier: 100 emails/day)
- An existing application to monitor (or use this dashboard standalone with test data)
- Basic understanding of security concepts like IP addresses and login attempts

## Step-by-step guide

### 1. Set up the security event database schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Prompt V0 to create the security_events, alert_rules, and blocked_ips tables with proper indexes for fast querying on event_type, severity, and timestamps.

```
// Paste this prompt into V0's AI chat:
// Create a Supabase schema for a security monitoring system:
// 1. security_events table: id (uuid PK), event_type (text NOT NULL — 'login_failed', 'permission_change', 'rate_limit', 'suspicious_ip'), severity (text — 'low', 'medium', 'high', 'critical'), user_id (uuid FK nullable), ip_address (inet), user_agent (text), metadata (jsonb), created_at (timestamptz DEFAULT now())
// 2. alert_rules table: id (uuid PK), name (text), event_type (text), threshold (int), window_minutes (int), notify_email (text), is_active (boolean DEFAULT true)
// 3. blocked_ips table: id (uuid PK), ip_address (inet UNIQUE), reason (text), blocked_at (timestamptz), expires_at (timestamptz nullable)
// Add indexes on event_type, severity, and created_at for security_events.
// Add RLS so only admin users can access these tables.
// Generate the SQL migration and seed with 50 sample security events across all severity levels.
```

> Pro tip: Enable Supabase Realtime on the security_events table in your Supabase Dashboard under Database > Replication. This is required for live-updating event logs in the dashboard.

**Expected result:** Three tables created with indexes, RLS policies for admin access, and 50 sample security events seeded across different event types and severity levels.

### 2. Build the event ingestion API and middleware

Create an API route that accepts security events and a Next.js middleware that logs every incoming request. The middleware captures IP, path, and user agent, sending them to the ingestion endpoint for threat analysis.

```
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

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

export async function POST(req: NextRequest) {
  const body = await req.json()
  const { event_type, severity, user_id, ip_address, user_agent, metadata } = body

  const { error } = await supabase.from('security_events').insert({
    event_type,
    severity: severity ?? 'low',
    user_id,
    ip_address,
    user_agent,
    metadata,
  })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  // Check alert rules
  const { data: rules } = await supabase
    .from('alert_rules')
    .select('*')
    .eq('event_type', event_type)
    .eq('is_active', true)

  for (const rule of rules ?? []) {
    const windowStart = new Date(
      Date.now() - rule.window_minutes * 60000
    ).toISOString()
    const { count } = await supabase
      .from('security_events')
      .select('*', { count: 'exact', head: true })
      .eq('event_type', event_type)
      .gte('created_at', windowStart)

    if ((count ?? 0) >= rule.threshold) {
      await fetch(process.env.SECURITY_WEBHOOK_URL!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `ALERT: ${rule.name} — ${count} ${event_type} events in ${rule.window_minutes} minutes`,
        }),
      }).catch(() => {})
    }
  }

  return NextResponse.json({ success: true })
}
```

**Expected result:** POST requests to /api/security/ingest create security events in Supabase. When an alert rule threshold is exceeded, a webhook notification fires to Slack or Discord.

### 3. Create the security dashboard with real-time metrics

Build the main dashboard page as a Server Component that displays key metrics (failed logins/hour, active alerts, blocked IPs) in Card components, with a Recharts AreaChart for the event timeline.

```
import { createClient } from '@/lib/supabase/server'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { EventTimeline } from '@/components/event-timeline'
import { LiveEventLog } from '@/components/live-event-log'

export default async function SecurityDashboard() {
  const supabase = await createClient()
  const oneHourAgo = new Date(Date.now() - 3600000).toISOString()

  const { count: failedLogins } = await supabase
    .from('security_events')
    .select('*', { count: 'exact', head: true })
    .eq('event_type', 'login_failed')
    .gte('created_at', oneHourAgo)

  const { count: activeAlerts } = await supabase
    .from('security_events')
    .select('*', { count: 'exact', head: true })
    .in('severity', ['high', 'critical'])
    .gte('created_at', oneHourAgo)

  const { count: blockedIPs } = await supabase
    .from('blocked_ips')
    .select('*', { count: 'exact', head: true })

  const { data: recentEvents } = await supabase
    .from('security_events')
    .select('*')
    .order('created_at', { ascending: false })
    .limit(100)

  return (
    <div className="p-6 space-y-6">
      <h1 className="text-3xl font-bold">Security Dashboard</h1>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        <Card>
          <CardHeader><CardTitle>Failed Logins / Hour</CardTitle></CardHeader>
          <CardContent>
            <p className="text-4xl font-bold">{failedLogins ?? 0}</p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader><CardTitle>Active Alerts</CardTitle></CardHeader>
          <CardContent>
            <p className="text-4xl font-bold text-orange-500">{activeAlerts ?? 0}</p>
          </CardContent>
        </Card>
        <Card>
          <CardHeader><CardTitle>Blocked IPs</CardTitle></CardHeader>
          <CardContent>
            <p className="text-4xl font-bold text-red-500">{blockedIPs ?? 0}</p>
          </CardContent>
        </Card>
      </div>
      <EventTimeline events={recentEvents ?? []} />
      <LiveEventLog initialEvents={recentEvents ?? []} />
    </div>
  )
}
```

> Pro tip: Use V0's Vars tab to store SECURITY_WEBHOOK_URL as a server-only secret (no NEXT_PUBLIC_ prefix). This can be a Slack Incoming Webhook URL or Discord webhook for instant alert notifications.

**Expected result:** A dashboard with three metric cards at the top, a timeline chart showing event frequency over the past 24 hours, and a live-updating event log at the bottom.

### 4. Add Supabase Realtime for live event streaming

Create a client component that subscribes to Supabase Realtime INSERT events on the security_events table. New events appear instantly in the log without refreshing the page.

```
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'

type SecurityEvent = {
  id: string
  event_type: string
  severity: string
  ip_address: string
  user_agent: string
  created_at: string
}

const severityColors: Record<string, string> = {
  low: 'bg-green-100 text-green-800',
  medium: 'bg-yellow-100 text-yellow-800',
  high: 'bg-orange-100 text-orange-800',
  critical: 'bg-red-100 text-red-800',
}

export function LiveEventLog({ initialEvents }: { initialEvents: SecurityEvent[] }) {
  const [events, setEvents] = useState<SecurityEvent[]>(initialEvents)
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase
      .channel('security-events')
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'security_events' },
        (payload) => {
          setEvents((prev) => [payload.new as SecurityEvent, ...prev.slice(0, 99)])
        }
      )
      .subscribe()

    return () => { supabase.removeChannel(channel) }
  }, [supabase])

  return (
    <div className="space-y-2">
      <h2 className="text-xl font-semibold">Live Event Log</h2>
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Time</TableHead>
            <TableHead>Type</TableHead>
            <TableHead>Severity</TableHead>
            <TableHead>IP Address</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {events.map((event) => (
            <TableRow key={event.id}>
              <TableCell className="text-sm">
                {new Date(event.created_at).toLocaleTimeString()}
              </TableCell>
              <TableCell>{event.event_type}</TableCell>
              <TableCell>
                <Badge className={severityColors[event.severity]}>
                  {event.severity}
                </Badge>
              </TableCell>
              <TableCell className="font-mono text-sm">{event.ip_address}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}
```

**Expected result:** The event log updates in real-time as new security events are ingested. Critical events appear with red badges and are prepended to the top of the list without page refresh.

### 5. Build the alert rules configuration page

Create a page where admins can define alert rules — setting thresholds like 'notify me when there are more than 10 failed logins in 5 minutes.' Rules are stored in Supabase and checked by the ingestion endpoint.

```
// Paste this prompt into V0's AI chat:
// Build an alert rules management page at app/security/rules/page.tsx.
// Requirements:
// - Fetch all alert_rules from Supabase and display in a shadcn/ui Table
// - Each row shows: rule name, event_type, threshold, window_minutes, notify_email, and a Switch for is_active toggle
// - Add a "Create Rule" Button that opens a Dialog with a Form containing:
//   - Input for rule name
//   - Select for event_type (login_failed, permission_change, rate_limit, suspicious_ip)
//   - Input type number for threshold
//   - Input type number for window_minutes
//   - Input for notify_email
// - Server Action to create/update/delete rules in Supabase
// - Use Badge to show event_type and color-code by severity association
// - Add an AlertDialog confirmation before deleting rules
// - Include a test button that simulates events to trigger the rule
```

**Expected result:** An alert rules page with a Table of existing rules, a Dialog for creating new rules, Switch toggles for enabling/disabling rules, and a delete confirmation AlertDialog.

### 6. Add the event timeline chart with Recharts

Create a Recharts AreaChart component that visualizes security events over time, grouped by severity. This gives a quick visual overview of when incidents spike.

```
'use client'

import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

type SecurityEvent = {
  id: string
  severity: string
  created_at: string
}

export function EventTimeline({ events }: { events: SecurityEvent[] }) {
  const hourlyData = events.reduce((acc, event) => {
    const hour = new Date(event.created_at).toLocaleTimeString([], {
      hour: '2-digit',
      minute: '2-digit',
    })
    const existing = acc.find((d) => d.time === hour)
    if (existing) {
      existing[event.severity] = (existing[event.severity] ?? 0) + 1
      existing.total += 1
    } else {
      acc.push({ time: hour, [event.severity]: 1, total: 1 })
    }
    return acc
  }, [] as Array<Record<string, any>>)

  return (
    <Card>
      <CardHeader>
        <CardTitle>Event Timeline</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width="100%" height={300}>
          <AreaChart data={hourlyData}>
            <XAxis dataKey="time" />
            <YAxis />
            <Tooltip />
            <Area type="monotone" dataKey="critical" stackId="1" fill="#ef4444" stroke="#ef4444" />
            <Area type="monotone" dataKey="high" stackId="1" fill="#f97316" stroke="#f97316" />
            <Area type="monotone" dataKey="medium" stackId="1" fill="#eab308" stroke="#eab308" />
            <Area type="monotone" dataKey="low" stackId="1" fill="#22c55e" stroke="#22c55e" />
          </AreaChart>
        </ResponsiveContainer>
      </CardContent>
    </Card>
  )
}
```

**Expected result:** A stacked area chart showing security events over time, color-coded by severity (red for critical, orange for high, yellow for medium, green for low).

## Complete code example

File: `app/api/security/ingest/route.ts`

```typescript
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

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

export async function POST(req: NextRequest) {
  const body = await req.json()
  const { event_type, severity, user_id, ip_address, user_agent, metadata } = body

  // Check if IP is blocked
  const { data: blocked } = await supabase
    .from('blocked_ips')
    .select('id')
    .eq('ip_address', ip_address)
    .gt('expires_at', new Date().toISOString())
    .maybeSingle()

  if (blocked) {
    return NextResponse.json({ blocked: true }, { status: 403 })
  }

  // Insert the security event
  const { error } = await supabase.from('security_events').insert({
    event_type,
    severity: severity ?? 'low',
    user_id,
    ip_address,
    user_agent,
    metadata,
  })

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }

  // Check alert rules for threshold breaches
  const { data: rules } = await supabase
    .from('alert_rules')
    .select('*')
    .eq('event_type', event_type)
    .eq('is_active', true)

  for (const rule of rules ?? []) {
    const windowStart = new Date(
      Date.now() - rule.window_minutes * 60000
    ).toISOString()

    const { count } = await supabase
      .from('security_events')
      .select('*', { count: 'exact', head: true })
      .eq('event_type', event_type)
      .gte('created_at', windowStart)

    if ((count ?? 0) >= rule.threshold && process.env.SECURITY_WEBHOOK_URL) {
      await fetch(process.env.SECURITY_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `ALERT: ${rule.name} triggered — ${count} events in ${rule.window_minutes}min`,
        }),
      }).catch(() => {})
    }
  }

  return NextResponse.json({ success: true })
}
```

## Common mistakes

- **Using NEXT_PUBLIC_ prefix for SUPABASE_SERVICE_ROLE_KEY in the ingestion endpoint** — The service role key bypasses all RLS policies. Exposing it with NEXT_PUBLIC_ makes it visible in the browser, allowing anyone to read, write, or delete any data in your database. Fix: Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without any prefix. It will only be available in server-side code (API routes, Server Actions, middleware).
- **Not enabling Supabase Realtime replication on the security_events table** — Supabase Realtime requires explicit opt-in per table. Without enabling it in the Supabase Dashboard, the live event log will never receive new events. Fix: Go to Supabase Dashboard, navigate to Database > Replication, and add security_events to the replication publication. Then the Realtime subscription in the client component will work.
- **Running alert rule checks synchronously in the ingestion endpoint** — Checking all rules after every event adds latency. If you have 20 rules, the ingestion endpoint becomes slow, and middleware logging blocks the original request. Fix: Return the response immediately after inserting the event. Check alert rules in a background process, or use Supabase database triggers to handle rule checking asynchronously.
- **Storing raw IP addresses without considering privacy regulations** — IP addresses are personally identifiable information under GDPR. Storing them indefinitely without a privacy policy or data retention plan creates legal liability. Fix: Add an expires_at or auto-delete policy on security_events older than 90 days. Mention IP logging in your privacy policy. Consider hashing IPs for anonymized analytics.

## Best practices

- Store SECURITY_WEBHOOK_URL and SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab as server-only secrets — never use NEXT_PUBLIC_ prefix for these
- Enable Supabase Realtime on security_events before deploying so the live event log works immediately in production
- Use indexes on event_type, severity, and created_at columns to keep dashboard queries fast even with millions of events
- Implement data retention — automatically delete or archive security events older than 90 days using a Vercel Cron Job
- Use Design Mode (Option+D) to color-code severity Badge components (red for critical, orange for high) at zero credit cost
- Add rate limiting to the ingestion endpoint itself to prevent a malicious actor from flooding your security_events table
- Use Server Components for the main dashboard page so metrics are computed server-side and the page loads quickly
- Test alert rules with sample events before going live to verify webhook notifications fire correctly

## Frequently asked questions

### Can I use this security dashboard to monitor an existing application?

Yes. The ingestion API at /api/security/ingest accepts POST requests from any application. Add a fetch call to your existing app's login handler, auth middleware, or error handler to send events to the dashboard. The dashboard is a standalone Next.js app that can monitor multiple services.

### How does Supabase Realtime work for live event updates?

Supabase Realtime uses WebSocket connections to stream database changes to connected clients. When a new row is inserted into security_events, the client component receives a postgres_changes event and prepends it to the local state. You need to enable Realtime replication on the table in Supabase Dashboard under Database > Replication.

### What V0 plan do I need for this security dashboard?

V0 Premium ($20/month) is recommended because this project involves multiple complex files — the dashboard, ingestion API, middleware, alert configuration, and Recharts components. Free tier works but you may run low on credits with the volume of prompts needed.

### How do I set up alert notifications to Slack?

Create a Slack Incoming Webhook URL in your Slack workspace settings. Add it as SECURITY_WEBHOOK_URL in V0's Vars tab (server-only, no NEXT_PUBLIC_ prefix). The ingestion endpoint sends a POST to this URL when alert thresholds are exceeded.

### Can this handle high event volumes in production?

Yes, with optimization. Supabase handles thousands of inserts per second with proper indexes. For very high volume, batch events in the middleware and flush periodically instead of one-by-one. Consider Supabase Pro for connection pooling via Supavisor.

### How do I deploy this to production?

Click Share in V0, then Publish to Production. After deployment, add your production webhook URL in Vercel Dashboard environment variables. Make sure SUPABASE_SERVICE_ROLE_KEY has no NEXT_PUBLIC_ prefix. The Supabase connection from Connect panel is automatically configured.

### Can RapidDev help build a custom security monitoring system?

Yes. RapidDev has built 600+ apps including security-critical applications with real-time monitoring, SIEM integrations, and compliance dashboards. Book a free consultation to discuss your security monitoring requirements and threat landscape.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/security-monitoring
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/security-monitoring
