# How to Build Privacy tools with V0

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

## TL;DR

Build a GDPR/CCPA compliance toolkit with V0 using Next.js and Supabase that gives users a self-service privacy center for managing consent, requesting data exports, and submitting deletion requests. Includes a cookie consent banner, audit logging, and admin review workflows — all in about 1-2 hours.

## Before you start

- A V0 account (Premium recommended for prompt queuing)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Clerk account for authentication (free tier works — users need to be logged in to manage privacy)
- A list of what personal data your app collects (email, name, usage data, etc.)

## Step-by-step guide

### 1. Set up the project and privacy schema

Open V0 and create a new project. Use the Connect panel to add Supabase, then prompt V0 to create the schema for consent records, data requests, and audit logging.

```
// Paste this prompt into V0's AI chat:
// Build a GDPR/CCPA privacy compliance toolkit. Create a Supabase schema with:
// 1. consent_records: id (uuid PK), user_id (uuid FK), consent_type (text check analytics/marketing/functional/essential), granted (boolean), ip_address (text), granted_at (timestamptz), revoked_at (timestamptz)
// 2. data_requests: id (uuid PK), user_id (uuid FK), request_type (text check export/deletion/access), status (text check pending/processing/completed/denied), completed_at (timestamptz), download_url (text), created_at (timestamptz)
// 3. audit_log: id (uuid PK), actor_id (uuid), action (text), entity_type (text), entity_id (uuid), details (jsonb), created_at (timestamptz)
// Add RLS: users can only read/write their own consent_records and data_requests. Audit log is insert-only for all, select for admins.
// Generate SQL migration and TypeScript types.
```

> Pro tip: Use V0's Git panel to connect to GitHub — this gives you version-controlled history of every privacy policy and consent flow change, which is valuable for regulatory audits.

**Expected result:** Supabase is connected with consent_records, data_requests, and audit_log tables created. RLS policies enforce per-user data access.

### 2. Build the consent management privacy center

Create the main privacy center page where users can see and manage their consent preferences. Each consent category gets a Switch toggle that immediately updates via a Server Action.

```
// Paste this prompt into V0's AI chat:
// Build a privacy center at app/privacy/page.tsx.
// Requirements:
// - Show a heading "Your Privacy Settings" with description about data control
// - Display consent categories in shadcn/ui Cards: Essential (always on, disabled Switch), Analytics, Marketing, Functional
// - Each Card has: category name, description of what data is collected, Switch toggle for granted/revoked
// - Switch toggles call a Server Action updateConsent() that updates consent_records and logs to audit_log
// - Below consent, show a "Your Data" section with two Cards:
//   - "Export My Data" Card with Button that creates a data_request with type=export
//   - "Delete My Data" Card with Button that opens AlertDialog ("This action cannot be undone") then creates a data_request with type=deletion
// - Show a Table of existing data_requests with Badge for status (pending=yellow, completed=green)
// - Add an Accordion at the bottom for privacy policy sections
// - Use Toast for confirmation feedback after each action
// - Server Components for data fetching, 'use client' for Switch toggles and AlertDialog
```

**Expected result:** A privacy center with Switch toggles for each consent category, export and delete request buttons, and a Table showing request history with status Badges.

### 3. Create the data export API route

Build an API route that aggregates all user data across tables into a JSON file. This uses SUPABASE_SERVICE_ROLE_KEY to bypass RLS and join across all tables containing user PII, then stores the export in Supabase Storage.

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

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

export async function POST(req: NextRequest) {
  const { userId } = await auth()
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { data: request } = await supabase
    .from('data_requests')
    .insert({ user_id: userId, request_type: 'export', status: 'processing' })
    .select()
    .single()

  const { data: profile } = await supabase
    .from('profiles')
    .select('*')
    .eq('user_id', userId)
    .single()

  const { data: consents } = await supabase
    .from('consent_records')
    .select('*')
    .eq('user_id', userId)

  const { data: activities } = await supabase
    .from('audit_log')
    .select('*')
    .eq('actor_id', userId)

  const exportData = {
    exported_at: new Date().toISOString(),
    profile,
    consent_records: consents,
    activity_log: activities,
  }

  const blob = new Blob([JSON.stringify(exportData, null, 2)], {
    type: 'application/json',
  })

  const filePath = `exports/${userId}/${request!.id}.json`
  await supabase.storage.from('data-exports').upload(filePath, blob)

  const { data: signedUrl } = await supabase.storage
    .from('data-exports')
    .createSignedUrl(filePath, 3600)

  await supabase
    .from('data_requests')
    .update({
      status: 'completed',
      completed_at: new Date().toISOString(),
      download_url: signedUrl?.signedUrl,
    })
    .eq('id', request!.id)

  await supabase.from('audit_log').insert({
    actor_id: userId,
    action: 'data_export_completed',
    entity_type: 'data_request',
    entity_id: request!.id,
    details: { file_path: filePath },
  })

  return NextResponse.json({ download_url: signedUrl?.signedUrl })
}
```

**Expected result:** POST /api/privacy/export generates a JSON file with all user data, uploads it to Supabase Storage, and returns a signed download URL valid for 1 hour.

### 4. Add the cookie consent banner with Next.js middleware

Create a middleware that checks for a consent cookie before allowing analytics and marketing scripts to load. This ensures no tracking happens before the user explicitly grants consent, which is a core GDPR requirement.

```
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  const consent = request.cookies.get('privacy-consent')?.value

  if (!consent) {
    response.headers.set('x-consent-status', 'none')
  } else {
    try {
      const parsed = JSON.parse(consent)
      response.headers.set(
        'x-consent-analytics',
        parsed.analytics ? 'granted' : 'denied'
      )
      response.headers.set(
        'x-consent-marketing',
        parsed.marketing ? 'granted' : 'denied'
      )
    } catch {
      response.headers.set('x-consent-status', 'invalid')
    }
  }

  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
```

> Pro tip: The cookie consent banner must load before any analytics scripts. Read the x-consent-analytics header in your layout.tsx to conditionally include Google Analytics or Plausible script tags.

**Expected result:** Every request gets consent status headers. Your layout can conditionally render analytics scripts based on these headers, ensuring no tracking before consent.

### 5. Build the admin audit log viewer

Create an admin page that displays the complete audit trail of all privacy-related actions. This is essential for demonstrating GDPR compliance during regulatory audits.

```
// Paste this prompt into V0's AI chat:
// Build an admin audit log page at app/admin/audit/page.tsx.
// Requirements:
// - Only accessible to admin users (check Clerk user role)
// - Fetch all audit_log entries ordered by created_at descending
// - Display in a shadcn/ui Table with columns: timestamp, actor_id, action, entity_type, entity_id
// - Action column uses Badge with color coding: data_export=blue, consent_update=green, deletion_request=red
// - Add date range filter using Calendar + Popover (DatePicker pattern)
// - Add Select filter for action type
// - Add pagination with 50 entries per page
// - Use Server Components for data fetching
// - Add a "Download Audit Log" Button that exports filtered results as CSV
```

**Expected result:** An admin audit log page showing all privacy actions in a filterable, paginated Table with color-coded action Badges and CSV export.

## Complete code example

File: `app/api/privacy/export/route.ts`

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

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

export async function POST(req: NextRequest) {
  const { userId } = await auth()
  if (!userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const { data: request } = await supabase
    .from('data_requests')
    .insert({
      user_id: userId,
      request_type: 'export',
      status: 'processing',
    })
    .select()
    .single()

  const [profileRes, consentsRes, activitiesRes] = await Promise.all([
    supabase.from('profiles').select('*').eq('user_id', userId).single(),
    supabase.from('consent_records').select('*').eq('user_id', userId),
    supabase.from('audit_log').select('*').eq('actor_id', userId),
  ])

  const exportData = {
    exported_at: new Date().toISOString(),
    profile: profileRes.data,
    consent_records: consentsRes.data,
    activity_log: activitiesRes.data,
  }

  const filePath = `exports/${userId}/${request!.id}.json`
  const blob = new Blob([JSON.stringify(exportData, null, 2)])
  await supabase.storage.from('data-exports').upload(filePath, blob)

  const { data: signed } = await supabase.storage
    .from('data-exports')
    .createSignedUrl(filePath, 3600)

  await supabase
    .from('data_requests')
    .update({
      status: 'completed',
      completed_at: new Date().toISOString(),
      download_url: signed?.signedUrl,
    })
    .eq('id', request!.id)

  return NextResponse.json({ download_url: signed?.signedUrl })
}
```

## Common mistakes

- **Loading analytics scripts before checking consent status** — GDPR requires explicit consent before any non-essential tracking. Loading scripts first and then checking consent still sends data on initial page load. Fix: Use Next.js middleware to set consent headers, then conditionally render script tags in layout.tsx based on the header values — scripts never load without consent.
- **Using SUPABASE_SERVICE_ROLE_KEY on the client side for data export** — The service role key bypasses all RLS policies. Exposing it in the browser gives anyone full database access. Fix: Only use SUPABASE_SERVICE_ROLE_KEY in API routes (app/api/*/route.ts) and Server Actions. Set it in the Vars tab without a NEXT_PUBLIC_ prefix.
- **Hard-deleting user data without an audit trail** — GDPR requires proof that you processed deletion requests. Without an audit log, you cannot demonstrate compliance during regulatory audits. Fix: Use soft-delete (mark records as deleted) and log every deletion action to the audit_log table before actually removing data. Keep audit logs for the legally required retention period.
- **Not including all tables in the data export** — GDPR data subject access requests require providing all personal data you hold. Missing tables means an incomplete export, which violates the regulation. Fix: Create a Supabase database function that joins across ALL tables containing user_id and returns a complete JSON object. Maintain a checklist of PII-containing tables and update the export function when adding new tables.

## Best practices

- Always load the cookie consent banner before any analytics or marketing scripts via Next.js middleware
- Log every privacy-related action (consent change, export, deletion) to the audit_log table for regulatory compliance
- Use Supabase Storage with signed URLs for data exports — links expire automatically after 1 hour
- Use V0's Git panel to connect to GitHub so all privacy flow changes are version-controlled and reviewable via PRs
- Set SUPABASE_SERVICE_ROLE_KEY in the Vars tab without NEXT_PUBLIC_ prefix — only use it server-side for data aggregation
- Implement soft-delete with a deleted_at timestamp before hard-deleting data to maintain audit trail integrity
- Use V0's Design Mode (Option+D) to visually adjust the consent banner and privacy center layout without spending credits

## Frequently asked questions

### Do I need a paid V0 plan to build privacy tools?

V0 Free works for the basic build, but Premium ($20/month) is recommended for prompt queuing to generate the consent banner, privacy center, and export endpoint more efficiently.

### Does this comply with GDPR and CCPA?

This build implements the core technical requirements: consent management, data export, deletion requests, and audit logging. However, you should consult a legal professional for your specific jurisdiction and data processing activities.

### How do I handle the cookie consent banner correctly?

The build uses Next.js middleware to check for a consent cookie on every request. Analytics and marketing scripts are only injected in layout.tsx when the corresponding consent flag is true. This ensures no tracking before explicit consent.

### Can users download their data immediately?

Yes. The export API route aggregates all user data in real time, uploads it to Supabase Storage, and returns a signed download URL. For large datasets, consider queuing the export as a background job and notifying the user when ready.

### How do I deploy this to production?

Click Share then Publish to Production in V0. Make sure SUPABASE_SERVICE_ROLE_KEY and CLERK_SECRET_KEY are set in the Vars tab without NEXT_PUBLIC_ prefix. Your cookie consent banner will work automatically on the Vercel domain.

### What if I add new tables that contain personal data?

Update the data export API route to include the new table in its aggregation query. Maintain a checklist of PII-containing tables and review it whenever you add new data models.

### Can RapidDev help build a custom privacy compliance toolkit?

Yes. RapidDev has built 600+ apps including privacy-compliant platforms with automated data retention, consent propagation to third parties, and multi-jurisdiction compliance. Book a free consultation to discuss your requirements.

---

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