Skip to main content
RapidDev - Software Development Agency

How to Build a Privacy Tools with Lovable

Build a GDPR and CCPA compliance toolkit in Lovable with consent record storage, a data export Edge Function, cascading user data deletion using the service_role key, and versioned privacy policies with acceptance tracking. Gives users full control over their data and gives you the audit trail regulators require.

What you'll build

  • Consent management system recording when users accepted each privacy policy version
  • Granular consent preferences (analytics, marketing, functional) stored per user
  • Data export Edge Function that compiles all user data into a JSON archive using the service_role
  • Cascading data deletion flow that removes all user data from every table and Supabase Auth
  • Versioned privacy policies with diff tracking between versions
  • Consent banner component that appears for new users or when policy version changes
  • Privacy dashboard page where users can view, download, and delete their account data
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read2–2.5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a GDPR and CCPA compliance toolkit in Lovable with consent record storage, a data export Edge Function, cascading user data deletion using the service_role key, and versioned privacy policies with acceptance tracking. Gives users full control over their data and gives you the audit trail regulators require.

What you're building

GDPR (EU) and CCPA (California) require that users can access their data, delete it, and withdraw consent. This toolkit provides the technical infrastructure for all three rights. It does not replace legal advice, but it provides the mechanics that compliance requires.

Consent is stored per user per policy version. When your privacy policy changes, you bump the version number. On next login, users who haven't accepted the new version see the consent banner. The consent_records table records exactly when each user accepted which version, providing an immutable audit trail.

Data export runs in an Edge Function with the service_role key so it can read across all tables regardless of RLS policies. It queries every table that contains the user's data, compiles the results into a structured JSON object, and returns it for download. This fulfills the GDPR 'right to access' (Article 15).

Deletion is also handled by an Edge Function. Because RLS policies cannot guarantee cascade across all tables (especially when some tables have service_role-only access), the deletion function explicitly deletes from each table by user_id before finally calling supabase.auth.admin.deleteUser() to remove the auth record. A deletion_requests table tracks the request and its completion.

Final result

A privacy compliance toolkit with consent management, data export, account deletion, and an audit trail — all integrated into the Lovable app.

Tech stack

LovablePrivacy dashboard frontend
Supabase Edge FunctionsData export and deletion (Deno)
SupabaseDatabase, Auth, service_role for deletion
shadcn/uiCards, AlertDialog, Badge, Switch, Tabs
react-hook-form + zodConsent preference forms

Prerequisites

  • Lovable Pro account for Edge Function generation
  • Supabase project with service_role key saved to Cloud tab → Secrets as SUPABASE_SERVICE_ROLE_KEY
  • List of all tables in your app that contain user data (needed for the export and deletion functions)

Build steps

1

Create the consent and privacy policy schema

Set up the tables that track privacy policy versions and user consent. The audit trail these tables create is what makes compliance demonstrable.

prompt.txt
1Build a privacy compliance toolkit. Create these Supabase tables:
2
3- privacy_policies: id, version (text, e.g. '2024-01'), content (text, full policy markdown), summary_of_changes (text), effective_date (date), created_at, is_current (bool default false)
4
5- consent_records: id, user_id (FK auth.users), policy_version (text), accepted_at (timestamptz), ip_address (text, for audit), user_agent (text, for audit), UNIQUE(user_id, policy_version)
6
7- consent_preferences: id, user_id (FK auth.users UNIQUE), analytics_consent (bool default false), marketing_consent (bool default false), functional_consent (bool default true), last_updated_at
8
9- deletion_requests: id, user_id, email (text, stored separately since user will be deleted), requested_at, status (pending|processing|completed|failed), completed_at, error_message
10
11RLS:
12- privacy_policies: public SELECT for is_current = true; admin INSERT/UPDATE
13- consent_records: user_id = auth.uid() for SELECT/INSERT; service_role for DELETE
14- consent_preferences: user_id = auth.uid() for SELECT/UPDATE
15- deletion_requests: user_id = auth.uid() for INSERT and SELECT; service_role for updates
16
17Create a view current_privacy_policy that returns the single row WHERE is_current = true.

Pro tip: Ask Lovable to create a trigger on privacy_policies that automatically sets is_current = false on all other rows when a new row is inserted with is_current = true. This ensures only one policy is ever marked as current.

Expected result: All four tables are created with RLS. A starter privacy policy row can be inserted in the Supabase SQL editor. The app shows a basic Privacy Settings page in the navigation.

2

Build the consent banner and preference center

Create the consent banner that appears for users who haven't accepted the current policy, and the preference center where users can adjust granular consent settings.

prompt.txt
1Build two consent components:
2
31. Consent Banner at src/components/privacy/ConsentBanner.tsx:
4 - Display at the bottom of the screen as a fixed sticky bar
5 - Fetch the current privacy policy version from current_privacy_policy view
6 - Check consent_records: if no row exists for current user + current version, show the banner
7 - Banner content: 'We updated our privacy policy. Please review and accept to continue.' with a link to the full policy
8 - Buttons: 'Accept All' and 'Customize'
9 - 'Accept All' inserts into consent_records with current version and sets all consent_preferences to true
10 - 'Customize' opens the preference center Dialog
11 - Once accepted, hide the banner using local state (don't show again until next policy version)
12
132. Consent Preference Center Dialog at src/components/privacy/ConsentPreferences.tsx:
14 - Three switches with labels and descriptions:
15 - Functional (required, Switch disabled and always on): 'Required for the app to function'
16 - Analytics (optional, Switch): 'Help us improve by tracking feature usage anonymously'
17 - Marketing (optional, Switch): 'Receive product updates and promotional emails'
18 - 'Save Preferences' Button: upserts consent_preferences and inserts/upserts consent_records for current policy version
19 - Show last updated date below the switches
20 - Link to privacy policy page

Expected result: A new user sees the consent banner at the bottom of the screen. Clicking 'Accept All' dismisses it and creates a consent_records row. Clicking 'Customize' opens the preference toggles.

3

Build the data export Edge Function

Create the Edge Function that compiles all of a user's data into a downloadable JSON archive. This fulfills GDPR Article 15 (right of access) and CCPA Section 1798.100.

supabase/functions/export-user-data/index.ts
1// supabase/functions/export-user-data/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const corsHeaders = {
6 'Access-Control-Allow-Origin': '*',
7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8}
9
10serve(async (req: Request) => {
11 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
12
13 const authHeader = req.headers.get('Authorization')
14 if (!authHeader) return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: corsHeaders })
15
16 const userClient = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', {
17 global: { headers: { Authorization: authHeader } },
18 })
19 const serviceClient = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '')
20
21 const { data: { user }, error: authError } = await userClient.auth.getUser()
22 if (authError || !user) return new Response(JSON.stringify({ error: 'Invalid session' }), { status: 401, headers: corsHeaders })
23
24 const userId = user.id
25
26 const [consentRecords, consentPreferences, deletionRequests] = await Promise.all([
27 serviceClient.from('consent_records').select('*').eq('user_id', userId),
28 serviceClient.from('consent_preferences').select('*').eq('user_id', userId).single(),
29 serviceClient.from('deletion_requests').select('*').eq('user_id', userId),
30 ])
31
32 // Add queries for all other app-specific user tables here
33 const exportData = {
34 export_date: new Date().toISOString(),
35 user: { id: userId, email: user.email, created_at: user.created_at },
36 consent_records: consentRecords.data ?? [],
37 consent_preferences: consentPreferences.data ?? null,
38 deletion_requests: deletionRequests.data ?? [],
39 // Add other tables here: profiles, posts, orders, etc.
40 }
41
42 return new Response(JSON.stringify(exportData, null, 2), {
43 headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Content-Disposition': `attachment; filename="data-export-${userId.slice(0, 8)}.json"` },
44 })
45})

Expected result: Calling the Edge Function with a valid auth token returns a JSON file download containing all user data. The response includes Content-Disposition header so the browser treats it as a file download.

4

Build the account deletion flow and privacy dashboard

Create the cascading deletion Edge Function and the privacy dashboard where users can view their data summary, request export, and permanently delete their account.

prompt.txt
1Build two features:
2
31. Account deletion Edge Function at supabase/functions/delete-user-data/index.ts:
4 - Authenticate the user the same way as the export function
5 - Insert a deletion_requests row with status='processing'
6 - Using the service_role client, delete all user data in this order:
7 a. Delete from all app-specific tables by user_id (consent_records, consent_preferences, all other user tables)
8 b. Delete any Supabase Storage objects owned by the user: list all objects with user_id in name, delete them
9 c. Call supabase.auth.admin.deleteUser(userId) to delete the auth user
10 - Update deletion_requests row: status='completed', completed_at=now()
11 - If any step fails, set status='failed' and error_message, do NOT proceed with auth deletion
12 - Return { success: true } on completion
13
142. Privacy Dashboard page at src/pages/PrivacyDashboard.tsx:
15 - Tabs: 'My Consent', 'My Data', 'Delete Account'
16 - My Consent tab: show all consent_records as a timeline, consent_preferences switches (edit in place)
17 - My Data tab: show counts per table (e.g. 'Profile: 1 record', 'Orders: 12 records'), 'Download My Data' Button that calls export-user-data Edge Function
18 - Delete Account tab: danger zone with red styling. Steps: 'This is permanent and cannot be undone', a checkbox 'I understand this is permanent', a text Input requiring the user to type their email, 'Delete My Account' Button (destructive). On confirm: calls delete-user-data Edge Function and redirects to a farewell page.

Expected result: The privacy dashboard shows three tabs. Downloading data triggers a JSON file download. The delete account flow requires email confirmation before proceeding.

Complete code

src/components/privacy/ConsentBanner.tsx
1import { useState, useEffect } from 'react'
2import { Button } from '@/components/ui/button'
3import { supabase } from '@/integrations/supabase/client'
4import { useAuth } from '@/hooks/useAuth'
5
6interface Policy { version: string; summary_of_changes: string }
7
8export function ConsentBanner() {
9 const { user } = useAuth()
10 const [showBanner, setShowBanner] = useState(false)
11 const [policy, setPolicy] = useState<Policy | null>(null)
12
13 useEffect(() => {
14 if (!user) return
15 async function checkConsent() {
16 const { data: currentPolicy } = await supabase.from('current_privacy_policy').select('version, summary_of_changes').single()
17 if (!currentPolicy) return
18 setPolicy(currentPolicy)
19 const { data: existing } = await supabase.from('consent_records').select('id').eq('user_id', user!.id).eq('policy_version', currentPolicy.version).single()
20 if (!existing) setShowBanner(true)
21 }
22 checkConsent()
23 }, [user])
24
25 async function acceptAll() {
26 if (!user || !policy) return
27 await Promise.all([
28 supabase.from('consent_records').upsert({ user_id: user.id, policy_version: policy.version, accepted_at: new Date().toISOString() }, { onConflict: 'user_id,policy_version' }),
29 supabase.from('consent_preferences').upsert({ user_id: user.id, analytics_consent: true, marketing_consent: true, functional_consent: true, last_updated_at: new Date().toISOString() }, { onConflict: 'user_id' }),
30 ])
31 setShowBanner(false)
32 }
33
34 if (!showBanner || !policy) return null
35
36 return (
37 <div className="fixed bottom-0 left-0 right-0 z-50 border-t bg-background p-4 shadow-lg">
38 <div className="mx-auto flex max-w-4xl items-start gap-4 sm:items-center">
39 <div className="flex-1 text-sm">
40 <p className="font-medium">We updated our privacy policy (v{policy.version})</p>
41 <p className="text-muted-foreground mt-0.5">{policy.summary_of_changes}</p>
42 </div>
43 <div className="flex shrink-0 gap-2">
44 <Button variant="outline" size="sm" onClick={() => setShowBanner(false)}>Customize</Button>
45 <Button size="sm" onClick={acceptAll}>Accept All</Button>
46 </div>
47 </div>
48 </div>
49 )
50}

Customization ideas

Cookie consent manager

Add a cookie_consents table that maps consent_preferences to specific cookie names and vendors. Generate a cookie consent banner that lists each third-party service (Google Analytics, Intercom, etc.) separately. Conditionally load third-party scripts based on the user's cookie consent settings using React lazy loading.

Data retention policies

Add a data_retention_rules table that defines how long each type of user data is kept (e.g. logs: 90 days, analytics: 1 year, account data: until deletion). A scheduled Edge Function runs weekly and deletes records older than their retention period. Show the retention policy to users in their privacy dashboard.

Right to portability in multiple formats

Extend the data export Edge Function to support multiple formats: JSON (current), CSV (flat export of each table), and a human-readable HTML report. Accept a format query parameter. The HTML report is formatted like a proper data disclosure document with sections per data category.

Consent withdrawal impact preview

When a user toggles off analytics or marketing consent, show a preview of what will be disabled: 'Turning off analytics will disable: usage tracking, performance monitoring, feature suggestions.' This makes consent meaningful rather than abstract checkbox-checking.

Common pitfalls

Pitfall: Using the anon key for the deletion Edge Function

How to avoid: Use the service_role key only inside Edge Functions (never in the frontend) for operations that need to bypass RLS. The delete-user-data function authenticates the user first, then uses the service_role client for all deletions. The service_role key should never appear in frontend code or VITE_ variables.

Pitfall: Deleting the auth user before deleting their data

How to avoid: Always delete application data (all user tables, storage objects) before deleting the auth user. The auth user deletion should be the last step. If any earlier step fails, do not proceed to auth deletion and set the deletion request status to 'failed' with the error details.

Pitfall: Not recording IP address and user agent with consent

How to avoid: Record the IP address and user agent in consent_records. In the consent acceptance function, pass these values from the frontend (the client knows its own IP via navigator and User-Agent header). Store ip_address as text (not inet type to avoid parsing issues) and user_agent as text.

Pitfall: Allowing consent banner dismissal without acceptance

How to avoid: Remove the X close button from the consent banner. The only options should be 'Accept All' or 'Customize'. If users need to access the site to read the full policy before deciding, allow a 'Read Policy First' link that opens the policy in a sheet without dismissing the banner.

Best practices

  • Consent records should be immutable. Never update an existing consent_records row — always insert a new row when consent is updated. This preserves the full history of consent changes for audit purposes.
  • Use a service_role Edge Function for both data export and deletion. These operations need to read and write across all tables, bypassing user-scoped RLS. Never expose the service_role key to the frontend.
  • Test the deletion function in a staging environment before deploying. Create a test user, populate all tables with their data, run the deletion function, and verify that every table is empty for that user_id. Document which tables are included.
  • Show users a data inventory in the privacy dashboard: a list of each data category and record count. This builds trust by showing transparency about what is stored, and fulfills the GDPR Article 15 right to know.
  • Store the exact text of the privacy policy in the privacy_policies table, not just a link to an external URL. If you link to a URL and the content changes, your consent records no longer accurately reflect what users consented to.
  • Implement a 30-day cooling-off period before processing deletion requests. Store the requested_at timestamp. Notify the user by email when the request is received and when it is processed. This gives users time to change their mind before data is permanently deleted.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a GDPR compliance system with a Supabase Edge Function that deletes all data for a user. I need to delete data from these tables in order: user_profiles, user_posts, user_comments, user_files (plus Supabase Storage objects), consent_records, consent_preferences, and finally call auth.admin.deleteUser(). Help me write a TypeScript function deleteAllUserData(userId: string, serviceClient: SupabaseClient) that deletes in the correct order, handles errors from each step, and returns a report of what was deleted with counts.

Lovable Prompt

Add a 'What we collect' transparency page at /privacy/data-we-collect. Show a structured list of all data categories we store with: category name (Account Info, Usage Analytics, etc.), description of what is stored, how long it is retained, why it is collected (legal basis), and whether it can be deleted. Style this as an accordion (shadcn/ui Accordion) so each category expands to show the full details. Link to this page from the privacy dashboard and the consent banner.

Build Prompt

In Supabase, create a function get_user_data_summary(p_user_id uuid) that returns a JSONB object summarizing all data stored for a user. The function should query each user table and return: { tableName: { count: int, oldest_record: timestamptz, newest_record: timestamptz } }. Use a SECURITY DEFINER function so it can be called by the export Edge Function. Include tables: consent_records, consent_preferences, and any other tables that have a user_id column.

Frequently asked questions

Does this toolkit make my app fully GDPR compliant?

This toolkit provides the technical infrastructure that GDPR requires: consent records, data access, data portability, and the right to erasure. However, full GDPR compliance also requires: a lawful basis for processing, a Data Protection Officer for certain organizations, data processing agreements with vendors, a cookie policy, a breach notification procedure, and potentially a Privacy Impact Assessment. This guide covers the technical implementation; consult a lawyer for legal compliance.

What is CCPA and how is it different from GDPR?

CCPA (California Consumer Privacy Act) applies to businesses serving California residents. It grants similar rights to GDPR: access, deletion, and portability. Key differences: CCPA has a specific 'Do Not Sell My Personal Information' requirement (add a notice and opt-out mechanism), CCPA applies to businesses above certain revenue thresholds, and GDPR requires a legal basis for processing while CCPA uses an opt-out model. The consent and deletion infrastructure in this guide satisfies both frameworks' technical requirements.

How do I handle the 'right to be forgotten' for data in backups?

Database backups are a known challenge for GDPR deletion requests. GDPR Article 17 allows a 'reasonable period' to process deletion requests (typically 30 days). For backups, document that backups are retained for a maximum period (e.g. 30 days) and that deleted user data will be removed from all backups within that window naturally. You don't need to manually remove specific users from every backup — just ensure old backups are deleted on schedule.

Can I use this with anonymous (non-authenticated) users?

For anonymous users, you can't use user_id as the identifier. Instead, use a UUID stored in localStorage as a session ID. Store consent records with this session ID instead of user_id. When an anonymous user creates an account, migrate their session consent records to their new user_id. For deletion, anonymous sessions are lower-risk since no identifying information is tied to them — a cookie deletion or localStorage clear effectively 'deletes' them.

How long should I retain consent records?

Consent records should be retained for the duration of the user's account plus a period after deletion to defend against legal claims. Common practice is 3-7 years after account deletion, depending on jurisdiction. Do not delete consent_records as part of the user deletion flow — archive them with user_id and email (captured before deletion) for your legal retention period, then delete them automatically via a data retention policy after that period.

What happens to data in Supabase Storage when I delete a user?

Supabase Storage objects are not automatically deleted when a user's auth record is deleted. The delete-user-data Edge Function must explicitly list and delete all Storage objects associated with the user. Use supabase.storage.from('bucket').list(userId + '/') to find user-specific objects (assuming you store them in a user_id-prefixed path) and then call supabase.storage.from('bucket').remove(paths) to delete them.

Do I need to show a consent banner for users who already accepted an older policy version?

Yes, if your new policy version contains material changes that affect user rights. Minor formatting updates may not require re-consent, but changes to data collection, third-party sharing, or retention periods do. The privacy_policies.summary_of_changes field helps you communicate what changed. Show the banner to any user whose consent_records do not include an entry for the new version number.

Can RapidDev help with more complex privacy compliance features?

RapidDev builds production Lovable apps including compliance toolkits with cookie consent managers, automated data retention, DSAR (Data Subject Access Request) workflows, and privacy impact assessments. Reach out if your compliance requirements go beyond what this guide covers.

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.