# How to Build Personalization system with V0

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

## TL;DR

Build a behavioral content personalization engine with V0 using Next.js, Supabase with pgvector, and A/B testing. You'll create a rule-based system that segments users by behavior, serves tailored content variants, and tracks experiment results — all in about 2-4 hours from your browser.

## Before you start

- A V0 account (Premium or higher recommended for prompt queuing)
- A Supabase project with pgvector extension enabled (free tier works)
- Basic understanding of user segmentation concepts
- A product or website where you want to personalize content

## Step-by-step guide

### 1. Set up the project and Supabase schema with pgvector

Open V0 and create a new project. Use the Connect panel to add Supabase, then prompt V0 to generate the full schema including pgvector for user embeddings. This foundation handles events, rules, segments, and A/B tests.

```
// Paste this prompt into V0's AI chat:
// Build a personalization engine. Create a Supabase schema with these tables:
// 1. user_profiles: id (uuid PK), user_id (uuid FK unique), preferences (jsonb), segments (text[]), created_at (timestamptz), updated_at (timestamptz)
// 2. events: id (uuid PK), user_id (uuid FK), event_type (text), event_data (jsonb), page_url (text), created_at (timestamptz)
// 3. personalization_rules: id (uuid PK), name (text), segment_criteria (jsonb), content_variant (jsonb), priority (integer), is_active (boolean), created_at (timestamptz)
// 4. ab_tests: id (uuid PK), rule_id (uuid FK), variant_key (text), impressions (integer default 0), conversions (integer default 0), created_at (timestamptz)
// Enable pgvector extension and add: user_embeddings (id uuid PK, user_id uuid FK, embedding vector(1536), updated_at timestamptz)
// Add RLS policies so only authenticated users can read their own profiles.
// Generate SQL migration and TypeScript types.
```

> Pro tip: Use V0's prompt queuing — queue the schema prompt first, then immediately queue the event tracker and rule engine prompts while the first one generates.

**Expected result:** Supabase is connected via the Connect panel with all five tables created, pgvector extension enabled, and TypeScript types generated.

### 2. Build the behavioral event tracking API

Create an API route that ingests user behavioral events. This endpoint is called from your frontend on every meaningful action — page views, clicks, purchases — and stores them in the events table for later segment computation.

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

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

export async function POST(req: NextRequest) {
  const { user_id, event_type, event_data, page_url } = await req.json()

  if (!user_id || !event_type) {
    return NextResponse.json(
      { error: 'user_id and event_type are required' },
      { status: 400 }
    )
  }

  const { error } = await supabase.from('events').insert({
    user_id,
    event_type,
    event_data: event_data ?? {},
    page_url: page_url ?? null,
  })

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

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

**Expected result:** POST requests to /api/events/track insert behavioral events into Supabase. Each event includes the user ID, event type, custom data, and page URL.

### 3. Create the personalization rule evaluation endpoint

Build the core engine that evaluates personalization rules against a user's segments. This endpoint checks which rules match the current user and returns the highest-priority content variant. Use Edge Runtime for minimal latency.

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

export const runtime = 'edge'

export async function GET(req: NextRequest) {
  const supabase = createClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  )

  const userId = req.nextUrl.searchParams.get('user_id')
  if (!userId) {
    return NextResponse.json({ error: 'user_id required' }, { status: 400 })
  }

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

  if (!profile) {
    return NextResponse.json({ variant: null, fallback: true })
  }

  const { data: rules } = await supabase
    .from('personalization_rules')
    .select('id, name, segment_criteria, content_variant, priority')
    .eq('is_active', true)
    .order('priority', { ascending: false })

  const matchedRule = rules?.find((rule) => {
    const criteria = rule.segment_criteria as { segments?: string[] }
    return criteria.segments?.some((s: string) => profile.segments?.includes(s))
  })

  if (matchedRule) {
    await supabase.rpc('increment_impressions', { rule_id: matchedRule.id })
  }

  return NextResponse.json({
    variant: matchedRule?.content_variant ?? null,
    rule_name: matchedRule?.name ?? null,
    fallback: !matchedRule,
  })
}
```

> Pro tip: Edge Runtime runs your evaluation endpoint on Vercel's global edge network, cutting latency to under 50ms for most users worldwide.

**Expected result:** GET /api/personalization/evaluate?user_id=xxx returns the highest-priority matching content variant for that user, or a fallback flag if no rules match.

### 4. Build the rule management admin dashboard

Prompt V0 to generate an admin interface where you create and manage personalization rules. Each rule targets specific user segments and defines a content variant to serve. Include priority ordering and active/inactive toggles.

```
// Paste this prompt into V0's AI chat:
// Build a personalization rule management dashboard at app/dashboard/personalization/page.tsx.
// Requirements:
// - Fetch all personalization_rules and display in a shadcn/ui Table
// - Each row shows: name, target segments (as Badges), priority (number), is_active (Switch toggle), created_at
// - Add a "Create Rule" Button that opens a Sheet from the right side
// - Sheet contains: Input for rule name, Select for target segments (multi-select), JSON editor for content_variant, Slider for priority (1-100)
// - Switch toggles should immediately update is_active via Server Action
// - Add Tabs at the top: "Rules" and "A/B Tests"
// - A/B Tests tab shows a Table with variant_key, impressions, conversions, conversion rate, and a Badge showing statistical significance (green if >95% confidence)
// - Use Recharts BarChart to visualize conversion rates across variants
// - Use Server Components for data fetching, 'use client' only for interactive elements
```

**Expected result:** A dashboard with Tabs for Rules and A/B Tests. Rules are displayed in a Table with Switch toggles. A Sheet opens for creating new rules with segment targeting and priority.

### 5. Implement segment computation with database functions

Create a Supabase database function that recomputes user segments based on their event history. This runs periodically via pg_cron and updates the user_profiles.segments array, which the evaluation endpoint reads for fast rule matching.

```
// Paste this prompt into V0's AI chat:
// Create a Supabase SQL migration for segment computation:
// 1. A database function compute_user_segments(target_user_id uuid) that:
//    - Counts events by type for the user in the last 30 days
//    - Assigns segments based on thresholds: 'power_user' (50+ events), 'active' (10-49 events), 'new_user' (<10 events)
//    - Checks event_data for purchase events to assign 'buyer' segment
//    - Updates user_profiles.segments array and updated_at timestamp
// 2. A wrapper function recompute_all_segments() that loops through all users with recent events
// 3. A pg_cron job that runs recompute_all_segments() every hour
// 4. An RPC function increment_impressions(rule_id uuid) that atomically increments ab_tests.impressions
// Also create an API route at app/api/segments/recompute/route.ts that manually triggers recomputation for testing.
```

> Pro tip: Use Supavisor connection pooling (the pooled connection string from Supabase) in your Vars tab to prevent connection exhaustion when serverless functions compute segments concurrently.

**Expected result:** Segments are automatically recomputed every hour. Users are tagged with segments like power_user, active, new_user, and buyer based on their behavioral data.

### 6. Add the client-side event tracking hook

Create a React hook that your frontend components use to track events. This hook sends events to the tracking API and handles batching for performance. It uses useEffect to avoid SSR issues since it needs browser APIs.

```
'use client'

import { useCallback, useRef, useEffect } from 'react'

interface TrackEvent {
  event_type: string
  event_data?: Record<string, unknown>
  page_url?: string
}

export function useEventTracker(userId: string | null) {
  const queue = useRef<TrackEvent[]>([])

  const flush = useCallback(async () => {
    if (!userId || queue.current.length === 0) return
    const events = [...queue.current]
    queue.current = []

    await Promise.allSettled(
      events.map((event) =>
        fetch('/api/events/track', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ user_id: userId, ...event }),
        })
      )
    )
  }, [userId])

  useEffect(() => {
    const interval = setInterval(flush, 5000)
    return () => {
      clearInterval(interval)
      flush()
    }
  }, [flush])

  const track = useCallback(
    (event_type: string, event_data?: Record<string, unknown>) => {
      queue.current.push({
        event_type,
        event_data,
        page_url: typeof window !== 'undefined' ? window.location.pathname : undefined,
      })
    },
    []
  )

  return { track }
}
```

**Expected result:** Components call track('button_click', { button: 'cta' }) and events are batched and sent to the API every 5 seconds for efficient network usage.

## Complete code example

File: `app/api/personalization/evaluate/route.ts`

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

export const runtime = 'edge'

export async function GET(req: NextRequest) {
  const supabase = createClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  )

  const userId = req.nextUrl.searchParams.get('user_id')
  if (!userId) {
    return NextResponse.json({ error: 'user_id required' }, { status: 400 })
  }

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

  if (!profile) {
    return NextResponse.json({ variant: null, fallback: true })
  }

  const { data: rules } = await supabase
    .from('personalization_rules')
    .select('id, name, segment_criteria, content_variant, priority')
    .eq('is_active', true)
    .order('priority', { ascending: false })

  const matchedRule = rules?.find((rule) => {
    const criteria = rule.segment_criteria as { segments?: string[] }
    return criteria.segments?.some((s: string) =>
      profile.segments?.includes(s)
    )
  })

  if (matchedRule) {
    await supabase.rpc('increment_impressions', {
      rule_id: matchedRule.id,
    })
  }

  return NextResponse.json({
    variant: matchedRule?.content_variant ?? null,
    rule_name: matchedRule?.name ?? null,
    fallback: !matchedRule,
  })
}
```

## Common mistakes

- **Exposing SUPABASE_SERVICE_ROLE_KEY with a NEXT_PUBLIC_ prefix** — The service role key bypasses RLS and gives full database access. Adding NEXT_PUBLIC_ would expose it in the browser bundle. Fix: Store SUPABASE_SERVICE_ROLE_KEY in the Vars tab without any prefix. Only use it in API routes and Server Actions, never in client components.
- **Evaluating rules client-side instead of server-side** — Client-side evaluation exposes your rule logic and segment data to users, and can be manipulated via browser dev tools. Fix: Always evaluate personalization rules in an API route or Server Action. The client only receives the final content variant, never the rules themselves.
- **Not using connection pooling for serverless segment computation** — Each serverless function invocation opens a new database connection. Under load, this quickly exhausts Supabase connection limits. Fix: Use the Supavisor pooled connection string from your Supabase project settings. Set this as SUPABASE_URL in the Vars tab.
- **Accessing window or localStorage in Server Components for event tracking** — Server Components render on the server where browser APIs do not exist, causing runtime errors. Fix: Wrap event tracking in a 'use client' component and access window inside useEffect to ensure it only runs in the browser.

## Best practices

- Use Edge Runtime for the evaluation endpoint to minimize personalization latency globally
- Batch event tracking calls on the client side to reduce network requests and improve page performance
- Store personalization rules in the database rather than code so non-technical team members can update them
- Use V0's prompt queuing to generate the event tracker, rule engine, and dashboard components in sequence without waiting
- Always compute segments server-side using Supabase database functions to keep business logic secure and performant
- Set up pg_cron for automatic segment recomputation rather than computing on every page load
- Use V0's Design Mode (Option+D) to visually adjust the admin dashboard layout without spending credits
- Test personalization rules with a small segment before rolling out to all users

## Frequently asked questions

### What is the minimum V0 plan needed for a personalization system?

V0 Free works for basic builds, but Premium ($20/month) is recommended because prompt queuing lets you generate multiple subsystems (event tracker, rule engine, dashboard) in sequence without waiting for each to finish.

### Do I need pgvector for a basic personalization system?

No. For rule-based personalization with segment matching, standard Supabase tables are sufficient. pgvector is only needed if you want embedding-based user similarity for advanced content-based or hybrid personalization.

### How do I handle personalization for anonymous users who haven't logged in?

Generate an anonymous ID using crypto.randomUUID() and store it in a cookie via Next.js middleware. Track events against this anonymous ID, then merge it with the authenticated user profile when they sign in.

### Will the personalization engine slow down my page loads?

No, if you use Edge Runtime for the evaluation endpoint. Edge functions run globally on Vercel's network with sub-50ms latency. Pre-computed segments in user_profiles mean the evaluation is a simple database lookup, not a complex computation.

### How do I deploy my personalization system to production?

Click Share then Publish to Production in V0 — it deploys to Vercel in 30-60 seconds. Alternatively, use the Git panel to connect to GitHub and create an auto-PR for team review before merging to production.

### Can I run A/B tests without a third-party tool like Optimizely?

Yes. This build includes a built-in A/B testing framework with impression counting, conversion tracking, and statistical significance calculation — all stored in your own Supabase database with no external dependencies.

### Can RapidDev help build a custom personalization system?

Yes. RapidDev has built 600+ apps including advanced personalization engines with real-time segment computation and ML-powered recommendation systems. Book a free consultation to discuss your specific requirements.

---

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