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
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
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.
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.
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.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const body = await req.json()11 const { event_type, severity, user_id, ip_address, user_agent, metadata } = body1213 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 })2122 if (error) {23 return NextResponse.json({ error: error.message }, { status: 500 })24 }2526 // Check alert rules27 const { data: rules } = await supabase28 .from('alert_rules')29 .select('*')30 .eq('event_type', event_type)31 .eq('is_active', true)3233 for (const rule of rules ?? []) {34 const windowStart = new Date(35 Date.now() - rule.window_minutes * 6000036 ).toISOString()37 const { count } = await supabase38 .from('security_events')39 .select('*', { count: 'exact', head: true })40 .eq('event_type', event_type)41 .gte('created_at', windowStart)4243 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 }5354 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.
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.
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'67export default async function SecurityDashboard() {8 const supabase = await createClient()9 const oneHourAgo = new Date(Date.now() - 3600000).toISOString()1011 const { count: failedLogins } = await supabase12 .from('security_events')13 .select('*', { count: 'exact', head: true })14 .eq('event_type', 'login_failed')15 .gte('created_at', oneHourAgo)1617 const { count: activeAlerts } = await supabase18 .from('security_events')19 .select('*', { count: 'exact', head: true })20 .in('severity', ['high', 'critical'])21 .gte('created_at', oneHourAgo)2223 const { count: blockedIPs } = await supabase24 .from('blocked_ips')25 .select('*', { count: 'exact', head: true })2627 const { data: recentEvents } = await supabase28 .from('security_events')29 .select('*')30 .order('created_at', { ascending: false })31 .limit(100)3233 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.
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.
1'use client'23import { 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'78type SecurityEvent = {9 id: string10 event_type: string11 severity: string12 ip_address: string13 user_agent: string14 created_at: string15}1617const 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}2324export function LiveEventLog({ initialEvents }: { initialEvents: SecurityEvent[] }) {25 const [events, setEvents] = useState<SecurityEvent[]>(initialEvents)26 const supabase = createClient()2728 useEffect(() => {29 const channel = supabase30 .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()3940 return () => { supabase.removeChannel(channel) }41 }, [supabase])4243 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.
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.
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 Table5// - Each row shows: rule name, event_type, threshold, window_minutes, notify_email, and a Switch for is_active toggle6// - Add a "Create Rule" Button that opens a Dialog with a Form containing:7// - Input for rule name8// - Select for event_type (login_failed, permission_change, rate_limit, suspicious_ip)9// - Input type number for threshold10// - Input type number for window_minutes11// - Input for notify_email12// - Server Action to create/update/delete rules in Supabase13// - Use Badge to show event_type and color-code by severity association14// - Add an AlertDialog confirmation before deleting rules15// - Include a test button that simulates events to trigger the ruleExpected 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.
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.
1'use client'23import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'4import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'56type SecurityEvent = {7 id: string8 severity: string9 created_at: string10}1112export 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) + 121 existing.total += 122 } else {23 acc.push({ time: hour, [event.severity]: 1, total: 1 })24 }25 return acc26 }, [] as Array<Record<string, any>>)2728 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
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const body = await req.json()11 const { event_type, severity, user_id, ip_address, user_agent, metadata } = body1213 // Check if IP is blocked14 const { data: blocked } = await supabase15 .from('blocked_ips')16 .select('id')17 .eq('ip_address', ip_address)18 .gt('expires_at', new Date().toISOString())19 .maybeSingle()2021 if (blocked) {22 return NextResponse.json({ blocked: true }, { status: 403 })23 }2425 // Insert the security event26 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 })3435 if (error) {36 return NextResponse.json({ error: error.message }, { status: 500 })37 }3839 // Check alert rules for threshold breaches40 const { data: rules } = await supabase41 .from('alert_rules')42 .select('*')43 .eq('event_type', event_type)44 .eq('is_active', true)4546 for (const rule of rules ?? []) {47 const windowStart = new Date(48 Date.now() - rule.window_minutes * 6000049 ).toISOString()5051 const { count } = await supabase52 .from('security_events')53 .select('*', { count: 'exact', head: true })54 .eq('event_type', event_type)55 .gte('created_at', windowStart)5657 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 }6768 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation