Skip to main content
RapidDev - Software Development Agency

How to Build Security monitoring with V0

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.

What you'll build

  • Real-time security event dashboard with metric cards showing failed logins per hour, active alerts, and blocked IPs
  • Event timeline visualization using Recharts AreaChart with severity-colored data points
  • Next.js middleware that logs every request's IP and path into security_events via a lightweight POST
  • Alert rule configuration UI with threshold-based triggers and email notifications
  • IP blocking system with manual block/unblock and automatic expiration
  • Live-updating event log using Supabase Realtime subscriptions without page refresh
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced13 min read2-4 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
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.

What you're building

Every production application needs visibility into security events — failed login attempts, suspicious IPs, privilege escalations, and API abuse. Without monitoring, you discover breaches after the damage is done. A security dashboard gives your team real-time awareness and the ability to respond instantly.

V0 makes building this feasible for a small team. You prompt V0 to generate the dashboard layout with Recharts charts, the event ingestion API, and the alert configuration UI. Supabase handles the database, Realtime subscriptions for live updates, and RLS for access control. The entire monitoring stack runs on Vercel serverless.

The architecture uses Next.js middleware for request-level logging, API routes for event ingestion and alert checking, Supabase Realtime for live event streaming to the dashboard, and Server Components for the main dashboard views. Recharts renders the event timeline and severity distribution.

Final result

A real-time security monitoring dashboard with event ingestion, severity-colored event logs, configurable alert rules with email notifications, IP blocking, and live-updating charts powered by Supabase Realtime.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
RechartsData Visualization
ResendEmail Notifications

Prerequisites

  • 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

Build steps

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.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a Supabase schema for a security monitoring system:
3// 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())
4// 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)
5// 3. blocked_ips table: id (uuid PK), ip_address (inet UNIQUE), reason (text), blocked_at (timestamptz), expires_at (timestamptz nullable)
6// Add indexes on event_type, severity, and created_at for security_events.
7// Add RLS so only admin users can access these tables.
8// 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.

app/api/security/ingest/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function POST(req: NextRequest) {
10 const body = await req.json()
11 const { event_type, severity, user_id, ip_address, user_agent, metadata } = body
12
13 const { error } = await supabase.from('security_events').insert({
14 event_type,
15 severity: severity ?? 'low',
16 user_id,
17 ip_address,
18 user_agent,
19 metadata,
20 })
21
22 if (error) {
23 return NextResponse.json({ error: error.message }, { status: 500 })
24 }
25
26 // Check alert rules
27 const { data: rules } = await supabase
28 .from('alert_rules')
29 .select('*')
30 .eq('event_type', event_type)
31 .eq('is_active', true)
32
33 for (const rule of rules ?? []) {
34 const windowStart = new Date(
35 Date.now() - rule.window_minutes * 60000
36 ).toISOString()
37 const { count } = await supabase
38 .from('security_events')
39 .select('*', { count: 'exact', head: true })
40 .eq('event_type', event_type)
41 .gte('created_at', windowStart)
42
43 if ((count ?? 0) >= rule.threshold) {
44 await fetch(process.env.SECURITY_WEBHOOK_URL!, {
45 method: 'POST',
46 headers: { 'Content-Type': 'application/json' },
47 body: JSON.stringify({
48 text: `ALERT: ${rule.name} — ${count} ${event_type} events in ${rule.window_minutes} minutes`,
49 }),
50 }).catch(() => {})
51 }
52 }
53
54 return NextResponse.json({ success: true })
55}

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.

app/security/page.tsx
1import { createClient } from '@/lib/supabase/server'
2import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3import { Badge } from '@/components/ui/badge'
4import { EventTimeline } from '@/components/event-timeline'
5import { LiveEventLog } from '@/components/live-event-log'
6
7export default async function SecurityDashboard() {
8 const supabase = await createClient()
9 const oneHourAgo = new Date(Date.now() - 3600000).toISOString()
10
11 const { count: failedLogins } = await supabase
12 .from('security_events')
13 .select('*', { count: 'exact', head: true })
14 .eq('event_type', 'login_failed')
15 .gte('created_at', oneHourAgo)
16
17 const { count: activeAlerts } = await supabase
18 .from('security_events')
19 .select('*', { count: 'exact', head: true })
20 .in('severity', ['high', 'critical'])
21 .gte('created_at', oneHourAgo)
22
23 const { count: blockedIPs } = await supabase
24 .from('blocked_ips')
25 .select('*', { count: 'exact', head: true })
26
27 const { data: recentEvents } = await supabase
28 .from('security_events')
29 .select('*')
30 .order('created_at', { ascending: false })
31 .limit(100)
32
33 return (
34 <div className="p-6 space-y-6">
35 <h1 className="text-3xl font-bold">Security Dashboard</h1>
36 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
37 <Card>
38 <CardHeader><CardTitle>Failed Logins / Hour</CardTitle></CardHeader>
39 <CardContent>
40 <p className="text-4xl font-bold">{failedLogins ?? 0}</p>
41 </CardContent>
42 </Card>
43 <Card>
44 <CardHeader><CardTitle>Active Alerts</CardTitle></CardHeader>
45 <CardContent>
46 <p className="text-4xl font-bold text-orange-500">{activeAlerts ?? 0}</p>
47 </CardContent>
48 </Card>
49 <Card>
50 <CardHeader><CardTitle>Blocked IPs</CardTitle></CardHeader>
51 <CardContent>
52 <p className="text-4xl font-bold text-red-500">{blockedIPs ?? 0}</p>
53 </CardContent>
54 </Card>
55 </div>
56 <EventTimeline events={recentEvents ?? []} />
57 <LiveEventLog initialEvents={recentEvents ?? []} />
58 </div>
59 )
60}

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.

components/live-event-log.tsx
1'use client'
2
3import { useEffect, useState } from 'react'
4import { createClient } from '@/lib/supabase/client'
5import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
6import { Badge } from '@/components/ui/badge'
7
8type SecurityEvent = {
9 id: string
10 event_type: string
11 severity: string
12 ip_address: string
13 user_agent: string
14 created_at: string
15}
16
17const severityColors: Record<string, string> = {
18 low: 'bg-green-100 text-green-800',
19 medium: 'bg-yellow-100 text-yellow-800',
20 high: 'bg-orange-100 text-orange-800',
21 critical: 'bg-red-100 text-red-800',
22}
23
24export function LiveEventLog({ initialEvents }: { initialEvents: SecurityEvent[] }) {
25 const [events, setEvents] = useState<SecurityEvent[]>(initialEvents)
26 const supabase = createClient()
27
28 useEffect(() => {
29 const channel = supabase
30 .channel('security-events')
31 .on(
32 'postgres_changes',
33 { event: 'INSERT', schema: 'public', table: 'security_events' },
34 (payload) => {
35 setEvents((prev) => [payload.new as SecurityEvent, ...prev.slice(0, 99)])
36 }
37 )
38 .subscribe()
39
40 return () => { supabase.removeChannel(channel) }
41 }, [supabase])
42
43 return (
44 <div className="space-y-2">
45 <h2 className="text-xl font-semibold">Live Event Log</h2>
46 <Table>
47 <TableHeader>
48 <TableRow>
49 <TableHead>Time</TableHead>
50 <TableHead>Type</TableHead>
51 <TableHead>Severity</TableHead>
52 <TableHead>IP Address</TableHead>
53 </TableRow>
54 </TableHeader>
55 <TableBody>
56 {events.map((event) => (
57 <TableRow key={event.id}>
58 <TableCell className="text-sm">
59 {new Date(event.created_at).toLocaleTimeString()}
60 </TableCell>
61 <TableCell>{event.event_type}</TableCell>
62 <TableCell>
63 <Badge className={severityColors[event.severity]}>
64 {event.severity}
65 </Badge>
66 </TableCell>
67 <TableCell className="font-mono text-sm">{event.ip_address}</TableCell>
68 </TableRow>
69 ))}
70 </TableBody>
71 </Table>
72 </div>
73 )
74}

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.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build an alert rules management page at app/security/rules/page.tsx.
3// Requirements:
4// - Fetch all alert_rules from Supabase and display in a shadcn/ui Table
5// - Each row shows: rule name, event_type, threshold, window_minutes, notify_email, and a Switch for is_active toggle
6// - Add a "Create Rule" Button that opens a Dialog with a Form containing:
7// - Input for rule name
8// - Select for event_type (login_failed, permission_change, rate_limit, suspicious_ip)
9// - Input type number for threshold
10// - Input type number for window_minutes
11// - Input for notify_email
12// - Server Action to create/update/delete rules in Supabase
13// - Use Badge to show event_type and color-code by severity association
14// - Add an AlertDialog confirmation before deleting rules
15// - 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.

components/event-timeline.tsx
1'use client'
2
3import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
4import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
5
6type SecurityEvent = {
7 id: string
8 severity: string
9 created_at: string
10}
11
12export function EventTimeline({ events }: { events: SecurityEvent[] }) {
13 const hourlyData = events.reduce((acc, event) => {
14 const hour = new Date(event.created_at).toLocaleTimeString([], {
15 hour: '2-digit',
16 minute: '2-digit',
17 })
18 const existing = acc.find((d) => d.time === hour)
19 if (existing) {
20 existing[event.severity] = (existing[event.severity] ?? 0) + 1
21 existing.total += 1
22 } else {
23 acc.push({ time: hour, [event.severity]: 1, total: 1 })
24 }
25 return acc
26 }, [] as Array<Record<string, any>>)
27
28 return (
29 <Card>
30 <CardHeader>
31 <CardTitle>Event Timeline</CardTitle>
32 </CardHeader>
33 <CardContent>
34 <ResponsiveContainer width="100%" height={300}>
35 <AreaChart data={hourlyData}>
36 <XAxis dataKey="time" />
37 <YAxis />
38 <Tooltip />
39 <Area type="monotone" dataKey="critical" stackId="1" fill="#ef4444" stroke="#ef4444" />
40 <Area type="monotone" dataKey="high" stackId="1" fill="#f97316" stroke="#f97316" />
41 <Area type="monotone" dataKey="medium" stackId="1" fill="#eab308" stroke="#eab308" />
42 <Area type="monotone" dataKey="low" stackId="1" fill="#22c55e" stroke="#22c55e" />
43 </AreaChart>
44 </ResponsiveContainer>
45 </CardContent>
46 </Card>
47 )
48}

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

app/api/security/ingest/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function POST(req: NextRequest) {
10 const body = await req.json()
11 const { event_type, severity, user_id, ip_address, user_agent, metadata } = body
12
13 // Check if IP is blocked
14 const { data: blocked } = await supabase
15 .from('blocked_ips')
16 .select('id')
17 .eq('ip_address', ip_address)
18 .gt('expires_at', new Date().toISOString())
19 .maybeSingle()
20
21 if (blocked) {
22 return NextResponse.json({ blocked: true }, { status: 403 })
23 }
24
25 // Insert the security event
26 const { error } = await supabase.from('security_events').insert({
27 event_type,
28 severity: severity ?? 'low',
29 user_id,
30 ip_address,
31 user_agent,
32 metadata,
33 })
34
35 if (error) {
36 return NextResponse.json({ error: error.message }, { status: 500 })
37 }
38
39 // Check alert rules for threshold breaches
40 const { data: rules } = await supabase
41 .from('alert_rules')
42 .select('*')
43 .eq('event_type', event_type)
44 .eq('is_active', true)
45
46 for (const rule of rules ?? []) {
47 const windowStart = new Date(
48 Date.now() - rule.window_minutes * 60000
49 ).toISOString()
50
51 const { count } = await supabase
52 .from('security_events')
53 .select('*', { count: 'exact', head: true })
54 .eq('event_type', event_type)
55 .gte('created_at', windowStart)
56
57 if ((count ?? 0) >= rule.threshold && process.env.SECURITY_WEBHOOK_URL) {
58 await fetch(process.env.SECURITY_WEBHOOK_URL, {
59 method: 'POST',
60 headers: { 'Content-Type': 'application/json' },
61 body: JSON.stringify({
62 text: `ALERT: ${rule.name} triggered — ${count} events in ${rule.window_minutes}min`,
63 }),
64 }).catch(() => {})
65 }
66 }
67
68 return NextResponse.json({ success: true })
69}

Customization ideas

Add geolocation IP mapping

Integrate a free IP geolocation API to plot security events on a world map, highlighting regions with concentrated suspicious activity.

Add automated IP blocking

When alert rules trigger, automatically add offending IPs to the blocked_ips table with a configurable expiration time instead of just sending notifications.

Add user behavior analytics

Track per-user patterns like login times, typical IP ranges, and session durations. Flag anomalies when a user logs in from an unusual location or at an unusual time.

Add PDF security reports

Generate weekly security summary PDFs with charts and tables using a library like @react-pdf/renderer, sent automatically via Resend email.

Integrate with PagerDuty or Opsgenie

Replace the simple webhook notification with PagerDuty or Opsgenie integration for critical alerts, enabling on-call rotation and escalation policies.

Common pitfalls

Pitfall: Using NEXT_PUBLIC_ prefix for SUPABASE_SERVICE_ROLE_KEY in the ingestion endpoint

How to avoid: 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).

Pitfall: Not enabling Supabase Realtime replication on the security_events table

How to avoid: 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.

Pitfall: Running alert rule checks synchronously in the ingestion endpoint

How to avoid: 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.

Pitfall: Storing raw IP addresses without considering privacy regulations

How to avoid: 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

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a security monitoring dashboard with Next.js App Router and Supabase. I need: 1) An event ingestion API that logs security events and checks alert rule thresholds, 2) A real-time dashboard with Recharts showing event timeline, 3) Supabase Realtime for live event streaming, 4) Alert rule configuration with threshold and time window. Help me design the schema and component architecture for handling high event volume.

Build Prompt

Create a Next.js middleware.ts at the project root that logs every incoming request as a security event. Capture the request IP from x-forwarded-for header, the path, user agent, and method. POST this data to /api/security/ingest as a lightweight fire-and-forget fetch. The middleware should not block or slow down the original request. Include logic to skip logging for static assets and the ingestion endpoint itself to avoid infinite loops.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.