Last updated: Aug 4, 2025, 11:26 AM UTC

SaaS Multi-Tenancy Development Guide

Status: Complete
Purpose: Comprehensive development guideline for building multi-tenant SaaS platforms
Critical: Prevents single-user architecture gaps and ensures proper SaaS account management


Why This Guide Is Critical

Build-v1 Lesson: NudgeCampaign was built as a single-user application with hardcoded DEMO_USER_ID instead of a proper multi-tenant SaaS platform. This guide ensures SaaS products implement proper multi-tenancy, user management, and account lifecycle from the start.

The Multi-Tenancy Gap: Technical teams often build single-user prototypes without considering the architectural requirements for serving multiple customers securely and scalably. This guide prevents the single-user vs. multi-tenant architecture mismatch.


Multi-Tenant Architecture Foundation

1. Database Architecture with Row Level Security (RLS)

Core Principle: Every data row must be associated with a user/tenant and isolated through database-level security policies.

Supabase Multi-Tenant Database Design

-- Users are managed by Supabase Auth (auth.users table)
-- All application data tables must include user_id foreign key

-- Example: Campaigns table with RLS
CREATE TABLE campaigns (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    description TEXT,
    status TEXT DEFAULT 'draft',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable Row Level Security
ALTER TABLE campaigns ENABLE ROW LEVEL SECURITY;

-- RLS Policy: Users can only access their own data
CREATE POLICY "Users can only access their own campaigns" 
ON campaigns FOR ALL 
USING (auth.uid() = user_id);

-- Insert policy for new records
CREATE POLICY "Users can insert campaigns for themselves" 
ON campaigns FOR INSERT 
WITH CHECK (auth.uid() = user_id);

Complete Multi-Tenant Schema Pattern

-- Core business entities with user isolation
CREATE TABLE contacts (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    email TEXT NOT NULL,
    first_name TEXT,
    last_name TEXT,
    company TEXT,
    tags TEXT[],
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE automations (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    name TEXT NOT NULL,
    n8n_workflow_id TEXT,
    trigger_type TEXT,
    is_active BOOLEAN DEFAULT false,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE email_sends (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    campaign_id UUID REFERENCES campaigns(id) ON DELETE CASCADE,
    contact_id UUID REFERENCES contacts(id) ON DELETE CASCADE,
    postmark_message_id TEXT,
    status TEXT DEFAULT 'pending',
    sent_at TIMESTAMPTZ,
    opened_at TIMESTAMPTZ,
    clicked_at TIMESTAMPTZ,
    bounced_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Apply RLS to all tables
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE automations ENABLE ROW LEVEL SECURITY;
ALTER TABLE email_sends ENABLE ROW LEVEL SECURITY;

-- RLS Policies for all tables
CREATE POLICY "contacts_user_isolation" ON contacts FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "automations_user_isolation" ON automations FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "email_sends_user_isolation" ON email_sends FOR ALL USING (auth.uid() = user_id);

User Profile and Subscription Management

-- Extended user profiles (beyond Supabase Auth)
CREATE TABLE user_profiles (
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
    company_name TEXT,
    website TEXT,
    industry TEXT,
    team_size TEXT,
    plan_type TEXT DEFAULT 'trial',
    subscription_status TEXT DEFAULT 'active',
    trial_ends_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Usage tracking for billing and limits
CREATE TABLE usage_metrics (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
    metric_type TEXT NOT NULL, -- 'emails_sent', 'contacts_stored', etc.
    metric_value INTEGER NOT NULL,
    period_start TIMESTAMPTZ NOT NULL,
    period_end TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Plan limits and restrictions
CREATE TABLE plan_limits (
    plan_type TEXT PRIMARY KEY,
    max_contacts INTEGER,
    max_emails_per_month INTEGER,
    max_automations INTEGER,
    features JSONB,
    price_cents INTEGER,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Insert plan definitions
INSERT INTO plan_limits (plan_type, max_contacts, max_emails_per_month, max_automations, features, price_cents) VALUES
('trial', 100, 500, 2, '{"advanced_analytics": false, "custom_domains": false, "api_access": false}', 0),
('professional', 10000, 10000, 10, '{"advanced_analytics": true, "custom_domains": false, "api_access": true}', 2900),
('enterprise', -1, -1, -1, '{"advanced_analytics": true, "custom_domains": true, "api_access": true, "priority_support": true}', 9900);

-- Apply RLS to user-specific tables
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE usage_metrics ENABLE ROW LEVEL SECURITY;

CREATE POLICY "user_profiles_self_access" ON user_profiles FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "usage_metrics_user_access" ON usage_metrics FOR ALL USING (auth.uid() = user_id);

2. Authentication and Session Management

Next.js 14 App Router with Supabase Auth

// middleware.ts - Protect routes and handle authentication
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })

  const {
    data: { session },
  } = await supabase.auth.getSession()

  // Redirect to login if not authenticated and accessing protected routes
  if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/auth/login', req.url))
  }

  // Redirect to dashboard if authenticated and accessing auth pages
  if (session && req.nextUrl.pathname.startsWith('/auth/')) {
    return NextResponse.redirect(new URL('/dashboard', req.url))
  }

  return res
}

export const config = {
  matcher: ['/dashboard/:path*', '/auth/:path*', '/admin/:path*']
}

Authentication Context Provider

// contexts/AuthContext.tsx
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import type { User, Session } from '@supabase/auth-helpers-nextjs'

interface AuthContextType {
  user: User | null
  session: Session | null
  userProfile: UserProfile | null
  loading: boolean
  signOut: () => Promise<void>
  refreshProfile: () => Promise<void>
}

interface UserProfile {
  user_id: string
  company_name?: string
  plan_type: string
  subscription_status: string
  trial_ends_at?: string
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [session, setSession] = useState<Session | null>(null)
  const [userProfile, setUserProfile] = useState<UserProfile | null>(null)
  const [loading, setLoading] = useState(true)
  
  const supabase = createClientComponentClient()

  const fetchUserProfile = async (userId: string) => {
    const { data, error } = await supabase
      .from('user_profiles')
      .select('*')
      .eq('user_id', userId)
      .single()
    
    if (data) setUserProfile(data)
  }

  const refreshProfile = async () => {
    if (user) {
      await fetchUserProfile(user.id)
    }
  }

  useEffect(() => {
    const getSession = async () => {
      const { data: { session } } = await supabase.auth.getSession()
      setSession(session)
      setUser(session?.user ?? null)
      
      if (session?.user) {
        await fetchUserProfile(session.user.id)
      }
      
      setLoading(false)
    }

    getSession()

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      async (event, session) => {
        setSession(session)
        setUser(session?.user ?? null)
        
        if (session?.user) {
          await fetchUserProfile(session.user.id)
        } else {
          setUserProfile(null)
        }
        
        setLoading(false)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  const signOut = async () => {
    await supabase.auth.signOut()
  }

  return (
    <AuthContext.Provider value={{
      user,
      session,
      userProfile,
      loading,
      signOut,
      refreshProfile
    }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

User Lifecycle Management

1. Self-Signup Flow

Registration Component with Company Information

// components/auth/SignupForm.tsx
'use client'
import { useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useRouter } from 'next/navigation'

interface SignupFormData {
  email: string
  password: string
  fullName: string
  companyName: string
  industry: string
  teamSize: string
}

export function SignupForm() {
  const [formData, setFormData] = useState<SignupFormData>({
    email: '',
    password: '',
    fullName: '',
    companyName: '',
    industry: '',
    teamSize: ''
  })
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  
  const supabase = createClientComponentClient()
  const router = useRouter()

  const handleSignup = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)

    try {
      // Create auth user
      const { data: authData, error: authError } = await supabase.auth.signUp({
        email: formData.email,
        password: formData.password,
        options: {
          data: {
            full_name: formData.fullName,
          },
          emailRedirectTo: `${window.location.origin}/auth/verify-email`
        }
      })

      if (authError) throw authError

      if (authData.user) {
        // Create user profile
        const { error: profileError } = await supabase
          .from('user_profiles')
          .insert({
            user_id: authData.user.id,
            company_name: formData.companyName,
            industry: formData.industry,
            team_size: formData.teamSize,
            plan_type: 'trial',
            subscription_status: 'active',
            trial_ends_at: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString() // 14 days
          })

        if (profileError) throw profileError

        // Initialize usage metrics
        await fetch('/api/users/initialize-usage', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId: authData.user.id })
        })

        router.push('/auth/verify-email')
      }
    } catch (error: any) {
      setError(error.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <form onSubmit={handleSignup} className="space-y-6">
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div>
          <label htmlFor="email" className="block text-sm font-medium mb-2">
            Email Address
          </label>
          <input
            id="email"
            type="email"
            required
            value={formData.email}
            onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
            className="w-full px-3 py-2 border border-gray-300 rounded-md"
          />
        </div>
        
        <div>
          <label htmlFor="fullName" className="block text-sm font-medium mb-2">
            Full Name
          </label>
          <input
            id="fullName"
            type="text"
            required
            value={formData.fullName}
            onChange={(e) => setFormData(prev => ({ ...prev, fullName: e.target.value }))}
            className="w-full px-3 py-2 border border-gray-300 rounded-md"
          />
        </div>
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium mb-2">
          Password
        </label>
        <input
          id="password"
          type="password"
          required
          minLength={8}
          value={formData.password}
          onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div>
          <label htmlFor="companyName" className="block text-sm font-medium mb-2">
            Company Name
          </label>
          <input
            id="companyName"
            type="text"
            required
            value={formData.companyName}
            onChange={(e) => setFormData(prev => ({ ...prev, companyName: e.target.value }))}
            className="w-full px-3 py-2 border border-gray-300 rounded-md"
          />
        </div>
        
        <div>
          <label htmlFor="teamSize" className="block text-sm font-medium mb-2">
            Team Size
          </label>
          <select
            id="teamSize"
            required
            value={formData.teamSize}
            onChange={(e) => setFormData(prev => ({ ...prev, teamSize: e.target.value }))}
            className="w-full px-3 py-2 border border-gray-300 rounded-md"
          >
            <option value="">Select team size</option>
            <option value="1">Just me</option>
            <option value="2-10">2-10 people</option>
            <option value="11-50">11-50 people</option>
            <option value="51-200">51-200 people</option>
            <option value="200+">200+ people</option>
          </select>
        </div>
      </div>

      <div>
        <label htmlFor="industry" className="block text-sm font-medium mb-2">
          Industry
        </label>
        <select
          id="industry"
          required
          value={formData.industry}
          onChange={(e) => setFormData(prev => ({ ...prev, industry: e.target.value }))}
          className="w-full px-3 py-2 border border-gray-300 rounded-md"
        >
          <option value="">Select industry</option>
          <option value="technology">Technology</option>
          <option value="marketing">Marketing & Advertising</option>
          <option value="ecommerce">E-commerce</option>
          <option value="healthcare">Healthcare</option>
          <option value="education">Education</option>
          <option value="finance">Finance</option>
          <option value="other">Other</option>
        </select>
      </div>

      {error && (
        <div className="p-3 bg-red-50 border border-red-200 rounded-md">
          <p className="text-red-600 text-sm">{error}</p>
        </div>
      )}

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {loading ? 'Creating Account...' : 'Start Free Trial'}
      </button>
    </form>
  )
}

Email Verification and Onboarding

// app/auth/verify-email/page.tsx
export default function VerifyEmailPage() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8">
        <div className="text-center">
          <h2 className="text-3xl font-bold text-gray-900">Check your email</h2>
          <p className="mt-2 text-gray-600">
            We've sent a verification link to your email address. Click the link to activate your account and complete your setup.
          </p>
        </div>
        
        <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
          <div className="flex">
            <div className="ml-3">
              <h3 className="text-sm font-medium text-blue-800">
                What happens next?
              </h3>
              <div className="mt-2 text-sm text-blue-700">
                <ul className="list-disc pl-5 space-y-1">
                  <li>Verify your email address</li>
                  <li>Complete your account setup</li>
                  <li>Take a quick product tour</li>
                  <li>Create your first campaign</li>
                </ul>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

2. Account Management Dashboard

Account Settings Component

// components/dashboard/AccountSettings.tsx
'use client'
import { useState, useEffect } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useAuth } from '@/contexts/AuthContext'

interface AccountFormData {
  fullName: string
  companyName: string
  website: string
  industry: string
  teamSize: string
}

export function AccountSettings() {
  const { user, userProfile, refreshProfile } = useAuth()
  const [formData, setFormData] = useState<AccountFormData>({
    fullName: '',
    companyName: '',
    website: '',
    industry: '',
    teamSize: ''
  })
  const [loading, setLoading] = useState(false)
  const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
  
  const supabase = createClientComponentClient()

  useEffect(() => {
    if (user && userProfile) {
      setFormData({
        fullName: user.user_metadata?.full_name || '',
        companyName: userProfile.company_name || '',
        website: userProfile.website || '',
        industry: userProfile.industry || '',
        teamSize: userProfile.team_size || ''
      })
    }
  }, [user, userProfile])

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setMessage(null)

    try {
      // Update auth user metadata
      const { error: authError } = await supabase.auth.updateUser({
        data: { full_name: formData.fullName }
      })

      if (authError) throw authError

      // Update user profile
      const { error: profileError } = await supabase
        .from('user_profiles')
        .update({
          company_name: formData.companyName,
          website: formData.website,
          industry: formData.industry,
          team_size: formData.teamSize,
          updated_at: new Date().toISOString()
        })
        .eq('user_id', user?.id)

      if (profileError) throw profileError

      await refreshProfile()
      setMessage({ type: 'success', text: 'Account updated successfully!' })
    } catch (error: any) {
      setMessage({ type: 'error', text: error.message })
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="bg-white shadow rounded-lg">
      <div className="px-4 py-5 sm:p-6">
        <h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
          Account Settings
        </h3>
        
        <form onSubmit={handleSubmit} className="space-y-6">
          <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div>
              <label htmlFor="fullName" className="block text-sm font-medium text-gray-700">
                Full Name
              </label>
              <input
                type="text"
                id="fullName"
                value={formData.fullName}
                onChange={(e) => setFormData(prev => ({ ...prev, fullName: e.target.value }))}
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
              />
            </div>
            
            <div>
              <label htmlFor="companyName" className="block text-sm font-medium text-gray-700">
                Company Name
              </label>
              <input
                type="text"
                id="companyName"
                value={formData.companyName}
                onChange={(e) => setFormData(prev => ({ ...prev, companyName: e.target.value }))}
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
              />
            </div>
          </div>

          <div>
            <label htmlFor="website" className="block text-sm font-medium text-gray-700">
              Website
            </label>
            <input
              type="url"
              id="website"
              value={formData.website}
              onChange={(e) => setFormData(prev => ({ ...prev, website: e.target.value }))}
              className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
              placeholder="https://www.example.com"
            />
          </div>

          <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div>
              <label htmlFor="industry" className="block text-sm font-medium text-gray-700">
                Industry
              </label>
              <select
                id="industry"
                value={formData.industry}
                onChange={(e) => setFormData(prev => ({ ...prev, industry: e.target.value }))}
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
              >
                <option value="">Select industry</option>
                <option value="technology">Technology</option>
                <option value="marketing">Marketing & Advertising</option>
                <option value="ecommerce">E-commerce</option>
                <option value="healthcare">Healthcare</option>
                <option value="education">Education</option>
                <option value="finance">Finance</option>
                <option value="other">Other</option>
              </select>
            </div>
            
            <div>
              <label htmlFor="teamSize" className="block text-sm font-medium text-gray-700">
                Team Size
              </label>
              <select
                id="teamSize"
                value={formData.teamSize}
                onChange={(e) => setFormData(prev => ({ ...prev, teamSize: e.target.value }))}
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
              >
                <option value="">Select team size</option>
                <option value="1">Just me</option>
                <option value="2-10">2-10 people</option>
                <option value="11-50">11-50 people</option>
                <option value="51-200">51-200 people</option>
                <option value="200+">200+ people</option>
              </select>
            </div>
          </div>

          {message && (
            <div className={`p-3 rounded-md ${
              message.type === 'success' 
                ? 'bg-green-50 border border-green-200' 
                : 'bg-red-50 border border-red-200'
            }`}>
              <p className={`text-sm ${
                message.type === 'success' ? 'text-green-600' : 'text-red-600'
              }`}>
                {message.text}
              </p>
            </div>
          )}

          <div className="flex justify-end">
            <button
              type="submit"
              disabled={loading}
              className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
            >
              {loading ? 'Saving...' : 'Save Changes'}
            </button>
          </div>
        </form>
      </div>
    </div>
  )
}

3. Subscription Status Management

Subscription Dashboard Component

// components/dashboard/SubscriptionDashboard.tsx
'use client'
import { useState, useEffect } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useAuth } from '@/contexts/AuthContext'

interface UsageData {
  contacts_stored: number
  emails_sent_this_month: number
  automations_active: number
}

interface PlanLimits {
  max_contacts: number
  max_emails_per_month: number
  max_automations: number
  features: Record<string, boolean>
  price_cents: number
}

export function SubscriptionDashboard() {
  const { userProfile } = useAuth()
  const [usage, setUsage] = useState<UsageData | null>(null)
  const [planLimits, setPlanLimits] = useState<PlanLimits | null>(null)
  const [loading, setLoading] = useState(true)
  
  const supabase = createClientComponentClient()

  useEffect(() => {
    const fetchData = async () => {
      if (!userProfile) return

      try {
        // Fetch current usage
        const { data: usageData } = await supabase
          .from('usage_metrics')
          .select('metric_type, metric_value')
          .eq('user_id', userProfile.user_id)
          .gte('period_start', new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString())

        // Fetch plan limits
        const { data: planData } = await supabase
          .from('plan_limits')
          .select('*')
          .eq('plan_type', userProfile.plan_type)
          .single()

        // Process usage data
        const usageMap = usageData?.reduce((acc, item) => {
          acc[item.metric_type] = item.metric_value
          return acc
        }, {} as Record<string, number>) || {}

        setUsage({
          contacts_stored: usageMap.contacts_stored || 0,
          emails_sent_this_month: usageMap.emails_sent || 0,
          automations_active: usageMap.automations_active || 0
        })

        setPlanLimits(planData)
      } catch (error) {
        console.error('Error fetching subscription data:', error)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [userProfile])

  if (loading) {
    return <div className="animate-pulse bg-gray-200 rounded-lg h-64"></div>
  }

  const formatPrice = (cents: number) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(cents / 100)
  }

  const getUsagePercentage = (current: number, limit: number) => {
    if (limit === -1) return 0 // Unlimited
    return Math.min((current / limit) * 100, 100)
  }

  const isTrialExpiring = userProfile?.trial_ends_at && 
    new Date(userProfile.trial_ends_at) < new Date(Date.now() + 3 * 24 * 60 * 60 * 1000)

  return (
    <div className="space-y-6">
      {/* Plan Status Card */}
      <div className="bg-white shadow rounded-lg p-6">
        <div className="flex items-center justify-between">
          <div>
            <h3 className="text-lg font-medium text-gray-900 capitalize">
              {userProfile?.plan_type} Plan
            </h3>
            <p className="text-sm text-gray-500">
              {planLimits && planLimits.price_cents > 0 
                ? `${formatPrice(planLimits.price_cents)}/month`
                : 'Free Trial'
              }
            </p>
            {userProfile?.plan_type === 'trial' && userProfile.trial_ends_at && (
              <p className={`text-sm ${isTrialExpiring ? 'text-red-600' : 'text-gray-500'}`}>
                Trial ends {new Date(userProfile.trial_ends_at).toLocaleDateString()}
              </p>
            )}
          </div>
          <div className="flex space-x-3">
            {userProfile?.plan_type === 'trial' && (
              <button className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
                Upgrade Plan
              </button>
            )}
            <button className="border border-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-50">
              Manage Billing
            </button>
          </div>
        </div>
      </div>

      {/* Usage Cards */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        <div className="bg-white shadow rounded-lg p-6">
          <div className="flex items-center">
            <div className="flex-1">
              <h4 className="text-sm font-medium text-gray-500">Contacts Stored</h4>
              <p className="text-2xl font-bold text-gray-900">
                {usage?.contacts_stored?.toLocaleString() || 0}
              </p>
              <p className="text-sm text-gray-500">
                {planLimits?.max_contacts === -1 
                  ? 'Unlimited' 
                  : `of ${planLimits?.max_contacts?.toLocaleString() || 0}`
                }
              </p>
            </div>
          </div>
          {planLimits && planLimits.max_contacts !== -1 && (
            <div className="mt-4">
              <div className="bg-gray-200 rounded-full h-2">
                <div 
                  className="bg-blue-600 h-2 rounded-full"
                  style={{ 
                    width: `${getUsagePercentage(usage?.contacts_stored || 0, planLimits.max_contacts)}%` 
                  }}
                ></div>
              </div>
            </div>
          )}
        </div>

        <div className="bg-white shadow rounded-lg p-6">
          <div className="flex items-center">
            <div className="flex-1">
              <h4 className="text-sm font-medium text-gray-500">Emails This Month</h4>
              <p className="text-2xl font-bold text-gray-900">
                {usage?.emails_sent_this_month?.toLocaleString() || 0}
              </p>
              <p className="text-sm text-gray-500">
                {planLimits?.max_emails_per_month === -1 
                  ? 'Unlimited' 
                  : `of ${planLimits?.max_emails_per_month?.toLocaleString() || 0}`
                }
              </p>
            </div>
          </div>
          {planLimits && planLimits.max_emails_per_month !== -1 && (
            <div className="mt-4">
              <div className="bg-gray-200 rounded-full h-2">
                <div 
                  className="bg-green-600 h-2 rounded-full"
                  style={{ 
                    width: `${getUsagePercentage(usage?.emails_sent_this_month || 0, planLimits.max_emails_per_month)}%` 
                  }}
                ></div>
              </div>
            </div>
          )}
        </div>

        <div className="bg-white shadow rounded-lg p-6">
          <div className="flex items-center">
            <div className="flex-1">
              <h4 className="text-sm font-medium text-gray-500">Active Automations</h4>
              <p className="text-2xl font-bold text-gray-900">
                {usage?.automations_active || 0}
              </p>
              <p className="text-sm text-gray-500">
                {planLimits?.max_automations === -1 
                  ? 'Unlimited' 
                  : `of ${planLimits?.max_automations || 0}`
                }
              </p>
            </div>
          </div>
          {planLimits && planLimits.max_automations !== -1 && (
            <div className="mt-4">
              <div className="bg-gray-200 rounded-full h-2">
                <div 
                  className="bg-purple-600 h-2 rounded-full"
                  style={{ 
                    width: `${getUsagePercentage(usage?.automations_active || 0, planLimits.max_automations)}%` 
                  }}
                ></div>
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Features List */}
      {planLimits && (
        <div className="bg-white shadow rounded-lg p-6">
          <h4 className="text-lg font-medium text-gray-900 mb-4">Plan Features</h4>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
            {Object.entries(planLimits.features).map(([feature, enabled]) => (
              <div key={feature} className="flex items-center">
                <div className={`w-4 h-4 rounded-full mr-3 ${enabled ? 'bg-green-500' : 'bg-gray-300'}`}></div>
                <span className={`text-sm ${enabled ? 'text-gray-900' : 'text-gray-500'}`}>
                  {feature.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  )
}

4. Account Deletion and GDPR Compliance

Account Deletion Component with Data Export

// components/dashboard/AccountDeletion.tsx
'use client'
import { useState } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useAuth } from '@/contexts/AuthContext'
import { useRouter } from 'next/navigation'

interface DataExportOptions {
  contacts: boolean
  campaigns: boolean
  email_history: boolean
  automations: boolean
}

export function AccountDeletion() {
  const { user } = useAuth()
  const [step, setStep] = useState<'options' | 'confirm' | 'processing'>('options')
  const [exportOptions, setExportOptions] = useState<DataExportOptions>({
    contacts: true,
    campaigns: true,
    email_history: false,
    automations: true
  })
  const [confirmText, setConfirmText] = useState('')
  const [loading, setLoading] = useState(false)
  
  const supabase = createClientComponentClient()
  const router = useRouter()

  const handleDataExport = async () => {
    setLoading(true)
    try {
      const response = await fetch('/api/users/export-data', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(exportOptions)
      })

      if (response.ok) {
        const blob = await response.blob()
        const url = window.URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = `data-export-${new Date().toISOString().split('T')[0]}.zip`
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
        window.URL.revokeObjectURL(url)
      }
    } catch (error) {
      console.error('Export failed:', error)
    } finally {
      setLoading(false)
    }
  }

  const handleAccountDeletion = async () => {
    if (confirmText !== 'DELETE MY ACCOUNT') {
      return
    }

    setStep('processing')
    setLoading(true)

    try {
      // Request account deletion
      const response = await fetch('/api/users/delete-account', {
        method: 'DELETE',
        headers: { 'Content-Type': 'application/json' }
      })

      if (response.ok) {
        // Sign out and redirect
        await supabase.auth.signOut()
        router.push('/auth/account-deleted')
      } else {
        throw new Error('Account deletion failed')
      }
    } catch (error) {
      console.error('Account deletion failed:', error)
      setStep('confirm')
    } finally {
      setLoading(false)
    }
  }

  if (step === 'processing') {
    return (
      <div className="bg-white shadow rounded-lg p-6">
        <div className="text-center">
          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-red-600 mx-auto mb-4"></div>
          <h3 className="text-lg font-medium text-gray-900">Deleting Account...</h3>
          <p className="text-gray-500">This may take a few moments</p>
        </div>
      </div>
    )
  }

  return (
    <div className="bg-white shadow rounded-lg">
      <div className="px-4 py-5 sm:p-6">
        <h3 className="text-lg leading-6 font-medium text-gray-900 text-red-600 mb-4">
          Delete Account
        </h3>
        
        {step === 'options' && (
          <div className="space-y-6">
            <div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
              <div className="flex">
                <div className="ml-3">
                  <h3 className="text-sm font-medium text-yellow-800">
                    Before you delete your account
                  </h3>
                  <div className="mt-2 text-sm text-yellow-700">
                    <p>
                      Account deletion is permanent and cannot be undone. We recommend exporting your data first.
                    </p>
                  </div>
                </div>
              </div>
            </div>

            <div>
              <h4 className="text-sm font-medium text-gray-900 mb-3">Export Your Data (GDPR Compliant)</h4>
              <div className="space-y-2">
                {Object.entries(exportOptions).map(([key, value]) => (
                  <label key={key} className="flex items-center">
                    <input
                      type="checkbox"
                      checked={value}
                      onChange={(e) => setExportOptions(prev => ({
                        ...prev,
                        [key]: e.target.checked
                      }))}
                      className="mr-2"
                    />
                    <span className="text-sm text-gray-700">
                      {key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
                    </span>
                  </label>
                ))}
              </div>
              
              <button
                onClick={handleDataExport}
                disabled={loading}
                className="mt-3 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
              >
                {loading ? 'Exporting...' : 'Export My Data'}
              </button>
            </div>

            <div className="border-t pt-6">
              <button
                onClick={() => setStep('confirm')}
                className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700"
              >
                Continue to Account Deletion
              </button>
            </div>
          </div>
        )}

        {step === 'confirm' && (
          <div className="space-y-6">
            <div className="bg-red-50 border border-red-200 rounded-md p-4">
              <div className="flex">
                <div className="ml-3">
                  <h3 className="text-sm font-medium text-red-800">
                    This action cannot be undone
                  </h3>
                  <div className="mt-2 text-sm text-red-700">
                    <ul className="list-disc pl-5 space-y-1">
                      <li>Your account will be permanently deleted</li>
                      <li>All your campaigns, contacts, and automations will be removed</li>
                      <li>Your subscription will be cancelled immediately</li>
                      <li>You will not be able to recover this data</li>
                    </ul>
                  </div>
                </div>
              </div>
            </div>

            <div>
              <label htmlFor="confirmText" className="block text-sm font-medium text-gray-700">
                Type "DELETE MY ACCOUNT" to confirm
              </label>
              <input
                type="text"
                id="confirmText"
                value={confirmText}
                onChange={(e) => setConfirmText(e.target.value)}
                className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
                placeholder="DELETE MY ACCOUNT"
              />
            </div>

            <div className="flex space-x-3">
              <button
                onClick={() => setStep('options')}
                className="border border-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-50"
              >
                Cancel
              </button>
              <button
                onClick={handleAccountDeletion}
                disabled={confirmText !== 'DELETE MY ACCOUNT' || loading}
                className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 disabled:opacity-50"
              >
                {loading ? 'Deleting...' : 'Delete My Account'}
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  )
}

πŸ‘¨β€ Admin Management System

1. Admin Dashboard with User Management

Admin User List Component

// components/admin/UserManagement.tsx
'use client'
import { useState, useEffect } from 'react'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'

interface AdminUser {
  id: string
  email: string
  created_at: string
  user_metadata: {
    full_name?: string
  }
  profile: {
    company_name?: string
    plan_type: string
    subscription_status: string
    trial_ends_at?: string
  }
}

export function UserManagement() {
  const [users, setUsers] = useState<AdminUser[]>([])
  const [loading, setLoading] = useState(true)
  const [searchTerm, setSearchTerm] = useState('')
  const [filterPlan, setFilterPlan] = useState<string>('all')
  const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
  
  const supabase = createClientComponentClient()

  useEffect(() => {
    fetchUsers()
  }, [])

  const fetchUsers = async () => {
    try {
      // Note: This would typically require admin-level RLS policies or a separate admin API
      const { data: profiles } = await supabase
        .from('user_profiles')
        .select(`
          user_id,
          company_name,
          plan_type,
          subscription_status,
          trial_ends_at,
          created_at
        `)
        .order('created_at', { ascending: false })

      // Fetch auth users (would need admin privileges)
      const response = await fetch('/api/admin/users')
      const authUsers = await response.json()

      // Combine auth and profile data
      const combinedUsers = authUsers.map((authUser: any) => {
        const profile = profiles?.find(p => p.user_id === authUser.id)
        return {
          ...authUser,
          profile: profile || {}
        }
      })

      setUsers(combinedUsers)
    } catch (error) {
      console.error('Error fetching users:', error)
    } finally {
      setLoading(false)
    }
  }

  const filteredUsers = users.filter(user => {
    const matchesSearch = user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
                         user.user_metadata?.full_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
                         user.profile?.company_name?.toLowerCase().includes(searchTerm.toLowerCase())
    
    const matchesPlan = filterPlan === 'all' || user.profile?.plan_type === filterPlan
    
    return matchesSearch && matchesPlan
  })

  const handleSuspendUser = async (userId: string) => {
    try {
      await fetch('/api/admin/users/suspend', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId })
      })
      
      await fetchUsers()
    } catch (error) {
      console.error('Error suspending user:', error)
    }
  }

  const handleChangePlan = async (userId: string, newPlan: string) => {
    try {
      await fetch('/api/admin/users/change-plan', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ userId, planType: newPlan })
      })
      
      await fetchUsers()
    } catch (error) {
      console.error('Error changing plan:', error)
    }
  }

  if (loading) {
    return <div className="animate-pulse bg-gray-200 rounded-lg h-96"></div>
  }

  return (
    <div className="space-y-6">
      {/* Filters */}
      <div className="bg-white shadow rounded-lg p-6">
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <div>
            <label htmlFor="search" className="block text-sm font-medium text-gray-700">
              Search Users
            </label>
            <input
              type="text"
              id="search"
              value={searchTerm}
              onChange={(e) => setSearchTerm(e.target.value)}
              placeholder="Search by email, name, or company..."
              className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
            />
          </div>
          
          <div>
            <label htmlFor="planFilter" className="block text-sm font-medium text-gray-700">
              Filter by Plan
            </label>
            <select
              id="planFilter"
              value={filterPlan}
              onChange={(e) => setFilterPlan(e.target.value)}
              className="mt-1 block w-full border-gray-300 rounded-md shadow-sm"
            >
              <option value="all">All Plans</option>
              <option value="trial">Trial</option>
              <option value="professional">Professional</option>
              <option value="enterprise">Enterprise</option>
            </select>
          </div>
        </div>
      </div>

      {/* User Stats */}
      <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
        <div className="bg-white shadow rounded-lg p-6">
          <div className="text-2xl font-bold text-gray-900">{users.length}</div>
          <div className="text-sm text-gray-500">Total Users</div>
        </div>
        <div className="bg-white shadow rounded-lg p-6">
          <div className="text-2xl font-bold text-blue-600">
            {users.filter(u => u.profile?.plan_type === 'trial').length}
          </div>
          <div className="text-sm text-gray-500">Trial Users</div>
        </div>
        <div className="bg-white shadow rounded-lg p-6">
          <div className="text-2xl font-bold text-green-600">
            {users.filter(u => u.profile?.plan_type === 'professional').length}
          </div>
          <div className="text-sm text-gray-500">Professional</div>
        </div>
        <div className="bg-white shadow rounded-lg p-6">
          <div className="text-2xl font-bold text-purple-600">
            {users.filter(u => u.profile?.plan_type === 'enterprise').length}
          </div>
          <div className="text-sm text-gray-500">Enterprise</div>
        </div>
      </div>

      {/* User Table */}
      <div className="bg-white shadow rounded-lg overflow-hidden">
        <table className="min-w-full divide-y divide-gray-200">
          <thead className="bg-gray-50">
            <tr>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                User
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Company
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Plan
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Status
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Created
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                Actions
              </th>
            </tr>
          </thead>
          <tbody className="bg-white divide-y divide-gray-200">
            {filteredUsers.map((user) => (
              <tr key={user.id} className="hover:bg-gray-50">
                <td className="px-6 py-4 whitespace-nowrap">
                  <div>
                    <div className="text-sm font-medium text-gray-900">
                      {user.user_metadata?.full_name || 'N/A'}
                    </div>
                    <div className="text-sm text-gray-500">{user.email}</div>
                  </div>
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                  {user.profile?.company_name || 'N/A'}
                </td>
                <td className="px-6 py-4 whitespace-nowrap">
                  <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
                    user.profile?.plan_type === 'trial' ? 'bg-blue-100 text-blue-800' :
                    user.profile?.plan_type === 'professional' ? 'bg-green-100 text-green-800' :
                    user.profile?.plan_type === 'enterprise' ? 'bg-purple-100 text-purple-800' :
                    'bg-gray-100 text-gray-800'
                  }`}>
                    {user.profile?.plan_type || 'unknown'}
                  </span>
                </td>
                <td className="px-6 py-4 whitespace-nowrap">
                  <span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
                    user.profile?.subscription_status === 'active' ? 'bg-green-100 text-green-800' :
                    user.profile?.subscription_status === 'suspended' ? 'bg-red-100 text-red-800' :
                    'bg-yellow-100 text-yellow-800'
                  }`}>
                    {user.profile?.subscription_status || 'unknown'}
                  </span>
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
                  {new Date(user.created_at).toLocaleDateString()}
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
                  <button
                    onClick={() => setSelectedUser(user)}
                    className="text-blue-600 hover:text-blue-900"
                  >
                    View
                  </button>
                  <button
                    onClick={() => handleSuspendUser(user.id)}
                    className="text-red-600 hover:text-red-900"
                  >
                    Suspend
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* User Detail Modal */}
      {selectedUser && (
        <UserDetailModal 
          user={selectedUser} 
          onClose={() => setSelectedUser(null)}
          onChangePlan={handleChangePlan}
        />
      )}
    </div>
  )
}

interface UserDetailModalProps {
  user: AdminUser
  onClose: () => void
  onChangePlan: (userId: string, newPlan: string) => void
}

function UserDetailModal({ user, onClose, onChangePlan }: UserDetailModalProps) {
  const [newPlan, setNewPlan] = useState(user.profile?.plan_type || '')

  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
      <div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-96 overflow-y-auto">
        <div className="flex justify-between items-center mb-4">
          <h3 className="text-lg font-medium text-gray-900">User Details</h3>
          <button
            onClick={onClose}
            className="text-gray-400 hover:text-gray-600"
          >
            βœ•
          </button>
        </div>
        
        <div className="space-y-4">
          <div className="grid grid-cols-2 gap-4">
            <div>
              <label className="block text-sm font-medium text-gray-700">Email</label>
              <p className="text-sm text-gray-900">{user.email}</p>
            </div>
            <div>
              <label className="block text-sm font-medium text-gray-700">Full Name</label>
              <p className="text-sm text-gray-900">{user.user_metadata?.full_name || 'N/A'}</p>
            </div>
          </div>
          
          <div className="grid grid-cols-2 gap-4">
            <div>
              <label className="block text-sm font-medium text-gray-700">Company</label>
              <p className="text-sm text-gray-900">{user.profile?.company_name || 'N/A'}</p>
            </div>
            <div>
              <label className="block text-sm font-medium text-gray-700">Status</label>
              <p className="text-sm text-gray-900">{user.profile?.subscription_status || 'N/A'}</p>
            </div>
          </div>

          <div>
            <label htmlFor="planSelect" className="block text-sm font-medium text-gray-700">
              Change Plan
            </label>
            <div className="mt-1 flex space-x-2">
              <select
                id="planSelect"
                value={newPlan}
                onChange={(e) => setNewPlan(e.target.value)}
                className="block w-full border-gray-300 rounded-md shadow-sm"
              >
                <option value="trial">Trial</option>
                <option value="professional">Professional</option>
                <option value="enterprise">Enterprise</option>
              </select>
              <button
                onClick={() => onChangePlan(user.id, newPlan)}
                className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
              >
                Update
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

2. Admin API Routes

Admin User Management API

// app/api/admin/users/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const supabase = createRouteHandlerClient({ cookies })

  try {
    // Verify admin access
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    // Check if user is admin (you would implement admin role checking)
    const { data: userProfile } = await supabase
      .from('user_profiles')
      .select('*')
      .eq('user_id', session.user.id)
      .single()

    if (userProfile?.role !== 'admin') {
      return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
    }

    // Get all users (requires admin privileges)
    // Note: This would typically use Supabase Admin API or RLS policies
    const { data: users, error } = await supabase.auth.admin.listUsers()

    if (error) throw error

    return NextResponse.json(users.users)
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}

User Suspension API

// app/api/admin/users/suspend/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const supabase = createRouteHandlerClient({ cookies })

  try {
    const { userId } = await request.json()

    // Verify admin access
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    // Update user profile to suspended status
    const { error } = await supabase
      .from('user_profiles')
      .update({ 
        subscription_status: 'suspended',
        updated_at: new Date().toISOString()
      })
      .eq('user_id', userId)

    if (error) throw error

    // Optionally disable the auth user
    // await supabase.auth.admin.updateUserById(userId, { banned: true })

    return NextResponse.json({ success: true })
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}

Plan Change API

// app/api/admin/users/change-plan/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const supabase = createRouteHandlerClient({ cookies })

  try {
    const { userId, planType } = await request.json()

    // Verify admin access
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    // Update user plan
    const { error } = await supabase
      .from('user_profiles')
      .update({ 
        plan_type: planType,
        updated_at: new Date().toISOString()
      })
      .eq('user_id', userId)

    if (error) throw error

    // Log the plan change
    await supabase
      .from('admin_actions')
      .insert({
        admin_user_id: session.user.id,
        target_user_id: userId,
        action_type: 'plan_change',
        action_details: { old_plan: '', new_plan: planType },
        created_at: new Date().toISOString()
      })

    return NextResponse.json({ success: true })
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}

Security & Compliance Implementation

1. Row Level Security (RLS) Policies

-- Comprehensive RLS policies for multi-tenant isolation

-- Enable RLS on all user tables
ALTER TABLE campaigns ENABLE ROW LEVEL SECURITY;
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE automations ENABLE ROW LEVEL SECURITY;
ALTER TABLE email_sends ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE usage_metrics ENABLE ROW LEVEL SECURITY;

-- User isolation policies
CREATE POLICY "campaigns_user_isolation" ON campaigns
    FOR ALL USING (auth.uid() = user_id);

CREATE POLICY "contacts_user_isolation" ON contacts
    FOR ALL USING (auth.uid() = user_id);

CREATE POLICY "automations_user_isolation" ON automations
    FOR ALL USING (auth.uid() = user_id);

CREATE POLICY "email_sends_user_isolation" ON email_sends
    FOR ALL USING (auth.uid() = user_id);

CREATE POLICY "user_profiles_self_access" ON user_profiles
    FOR ALL USING (auth.uid() = user_id);

CREATE POLICY "usage_metrics_user_access" ON usage_metrics
    FOR ALL USING (auth.uid() = user_id);

-- Admin access policies
CREATE POLICY "admin_full_access_campaigns" ON campaigns
    FOR ALL USING (
        EXISTS (
            SELECT 1 FROM user_profiles 
            WHERE user_id = auth.uid() 
            AND role = 'admin'
        )
    );

CREATE POLICY "admin_full_access_contacts" ON contacts
    FOR ALL USING (
        EXISTS (
            SELECT 1 FROM user_profiles 
            WHERE user_id = auth.uid() 
            AND role = 'admin'
        )
    );

-- Audit logging table with admin access
CREATE TABLE admin_actions (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    admin_user_id UUID REFERENCES auth.users(id),
    target_user_id UUID REFERENCES auth.users(id),
    action_type TEXT NOT NULL,
    action_details JSONB,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE admin_actions ENABLE ROW LEVEL SECURITY;

CREATE POLICY "admin_actions_admin_only" ON admin_actions
    FOR ALL USING (
        EXISTS (
            SELECT 1 FROM user_profiles 
            WHERE user_id = auth.uid() 
            AND role = 'admin'
        )
    );

2. Data Validation and Sanitization

// utils/validation.ts
import { z } from 'zod'

export const signupSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  fullName: z.string().min(1, 'Full name is required'),
  companyName: z.string().min(1, 'Company name is required'),
  industry: z.string().min(1, 'Industry is required'),
  teamSize: z.string().min(1, 'Team size is required')
})

export const accountUpdateSchema = z.object({
  fullName: z.string().min(1, 'Full name is required'),
  companyName: z.string().min(1, 'Company name is required'),
  website: z.string().url('Invalid website URL').optional().or(z.literal('')),
  industry: z.string().min(1, 'Industry is required'),
  teamSize: z.string().min(1, 'Team size is required')
})

export const campaignSchema = z.object({
  name: z.string().min(1, 'Campaign name is required').max(100, 'Name too long'),
  description: z.string().max(500, 'Description too long').optional(),
  subject: z.string().min(1, 'Subject is required').max(200, 'Subject too long'),
  content: z.string().min(1, 'Content is required'),
  status: z.enum(['draft', 'scheduled', 'sent', 'paused'])
})

export const contactSchema = z.object({
  email: z.string().email('Invalid email address'),
  firstName: z.string().max(50, 'First name too long').optional(),
  lastName: z.string().max(50, 'Last name too long').optional(),
  company: z.string().max(100, 'Company name too long').optional(),
  tags: z.array(z.string()).optional()
})

3. Usage Tracking and Limits Enforcement

// utils/usageTracking.ts
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'

export class UsageTracker {
  private supabase = createClientComponentClient()

  async checkContactLimit(userId: string): Promise<boolean> {
    // Get user's plan limits
    const { data: profile } = await this.supabase
      .from('user_profiles')
      .select('plan_type')
      .eq('user_id', userId)
      .single()

    const { data: planLimits } = await this.supabase
      .from('plan_limits')
      .select('max_contacts')
      .eq('plan_type', profile?.plan_type)
      .single()

    if (planLimits?.max_contacts === -1) return true // Unlimited

    // Get current contact count
    const { count } = await this.supabase
      .from('contacts')
      .select('*', { count: 'exact', head: true })
      .eq('user_id', userId)

    return (count || 0) < planLimits?.max_contacts
  }

  async checkEmailLimit(userId: string): Promise<boolean> {
    const { data: profile } = await this.supabase
      .from('user_profiles')
      .select('plan_type')
      .eq('user_id', userId)
      .single()

    const { data: planLimits } = await this.supabase
      .from('plan_limits')
      .select('max_emails_per_month')
      .eq('plan_type', profile?.plan_type)
      .single()

    if (planLimits?.max_emails_per_month === -1) return true // Unlimited

    // Get current month's email count
    const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1)
    const { data: usage } = await this.supabase
      .from('usage_metrics')
      .select('metric_value')
      .eq('user_id', userId)
      .eq('metric_type', 'emails_sent')
      .gte('period_start', startOfMonth.toISOString())
      .single()

    return (usage?.metric_value || 0) < planLimits?.max_emails_per_month
  }

  async recordUsage(userId: string, metricType: string, value: number = 1) {
    const now = new Date()
    const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
    const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)

    // Update or insert usage metric
    const { error } = await this.supabase
      .from('usage_metrics')
      .upsert({
        user_id: userId,
        metric_type: metricType,
        metric_value: value,
        period_start: startOfMonth.toISOString(),
        period_end: endOfMonth.toISOString()
      }, {
        onConflict: 'user_id,metric_type,period_start'
      })

    if (error) {
      console.error('Error recording usage:', error)
    }
  }
}

export const usageTracker = new UsageTracker()

Business Model Integration

1. Subscription Tier Management

// components/billing/PlanSelector.tsx
'use client'
import { useState } from 'react'
import { useAuth } from '@/contexts/AuthContext'

interface PlanFeatures {
  [key: string]: boolean
}

interface Plan {
  type: string
  name: string
  price: number
  features: PlanFeatures
  maxContacts: number
  maxEmails: number
  maxAutomations: number
  popular?: boolean
}

const plans: Plan[] = [
  {
    type: 'trial',
    name: 'Free Trial',
    price: 0,
    maxContacts: 100,
    maxEmails: 500,
    maxAutomations: 2,
    features: {
      'Email campaigns': true,
      'Basic automation': true,
      'Contact management': true,
      'Basic analytics': true,
      'Email support': true,
      'Advanced analytics': false,
      'Custom domains': false,
      'API access': false,
      'Priority support': false
    }
  },
  {
    type: 'professional',
    name: 'Professional',
    price: 29,
    maxContacts: 10000,
    maxEmails: 10000,
    maxAutomations: 10,
    popular: true,
    features: {
      'Email campaigns': true,
      'Advanced automation': true,
      'Contact management': true,
      'Advanced analytics': true,
      'Email support': true,
      'API access': true,
      'Custom domains': false,
      'Priority support': false
    }
  },
  {
    type: 'enterprise',
    name: 'Enterprise',
    price: 99,
    maxContacts: -1, // Unlimited
    maxEmails: -1, // Unlimited
    maxAutomations: -1, // Unlimited
    features: {
      'Everything in Professional': true,
      'Unlimited contacts': true,
      'Unlimited emails': true,
      'Custom domains': true,
      'Priority support': true,
      'Dedicated success manager': true,
      'Custom integrations': true,
      'SLA guarantee': true
    }
  }
]

export function PlanSelector() {
  const { userProfile } = useAuth()
  const [selectedPlan, setSelectedPlan] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)

  const handleUpgrade = async (planType: string) => {
    setLoading(true)
    try {
      // Integrate with Stripe or other payment processor
      const response = await fetch('/api/billing/create-checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ planType })
      })

      const { checkoutUrl } = await response.json()
      
      if (checkoutUrl) {
        window.location.href = checkoutUrl
      }
    } catch (error) {
      console.error('Error creating checkout:', error)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="space-y-8">
      <div className="text-center">
        <h2 className="text-3xl font-bold text-gray-900">Choose Your Plan</h2>
        <p className="mt-2 text-gray-600">
          Start with a free trial, upgrade anytime
        </p>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
        {plans.map((plan) => (
          <div
            key={plan.type}
            className={`relative rounded-lg border ${
              plan.popular
                ? 'border-blue-500 shadow-lg'
                : 'border-gray-200'
            } p-6`}
          >
            {plan.popular && (
              <div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
                <span className="bg-blue-500 text-white px-3 py-1 text-sm rounded-full">
                  Most Popular
                </span>
              </div>
            )}

            <div className="text-center">
              <h3 className="text-xl font-semibold text-gray-900">{plan.name}</h3>
              <div className="mt-2">
                <span className="text-3xl font-bold text-gray-900">
                  ${plan.price}
                </span>
                {plan.price > 0 && (
                  <span className="text-gray-500">/month</span>
                )}
              </div>
            </div>

            <div className="mt-6">
              <div className="text-sm text-gray-600 space-y-1">
                <div>
                  {plan.maxContacts === -1 ? 'Unlimited' : plan.maxContacts.toLocaleString()} contacts
                </div>
                <div>
                  {plan.maxEmails === -1 ? 'Unlimited' : plan.maxEmails.toLocaleString()} emails/month
                </div>
                <div>
                  {plan.maxAutomations === -1 ? 'Unlimited' : plan.maxAutomations} automations
                </div>
              </div>
            </div>

            <ul className="mt-6 space-y-3">
              {Object.entries(plan.features).map(([feature, included]) => (
                <li key={feature} className="flex items-center">
                  <div className={`w-4 h-4 rounded-full mr-3 ${
                    included ? 'bg-green-500' : 'bg-gray-300'
                  }`}></div>
                  <span className={`text-sm ${
                    included ? 'text-gray-900' : 'text-gray-500'
                  }`}>
                    {feature}
                  </span>
                </li>
              ))}
            </ul>

            <div className="mt-8">
              {userProfile?.plan_type === plan.type ? (
                <div className="text-center py-2 text-green-600 font-medium">
                  Current Plan
                </div>
              ) : (
                <button
                  onClick={() => handleUpgrade(plan.type)}
                  disabled={loading}
                  className={`w-full py-2 px-4 rounded-md font-medium ${
                    plan.popular
                      ? 'bg-blue-600 text-white hover:bg-blue-700'
                      : 'border border-gray-300 text-gray-700 hover:bg-gray-50'
                  } disabled:opacity-50`}
                >
                  {plan.type === 'trial' ? 'Start Free Trial' : 'Upgrade'}
                </button>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

2. Billing Integration with Stripe

// app/api/billing/create-checkout/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16'
})

export async function POST(request: NextRequest) {
  const supabase = createRouteHandlerClient({ cookies })

  try {
    const { planType } = await request.json()

    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    // Get plan details
    const { data: planLimits } = await supabase
      .from('plan_limits')
      .select('*')
      .eq('plan_type', planType)
      .single()

    if (!planLimits) {
      return NextResponse.json({ error: 'Plan not found' }, { status: 404 })
    }

    // Create Stripe checkout session
    const checkoutSession = await stripe.checkout.sessions.create({
      mode: 'subscription',
      payment_method_types: ['card'],
      customer_email: session.user.email,
      line_items: [
        {
          price_data: {
            currency: 'usd',
            product_data: {
              name: `${planType.charAt(0).toUpperCase() + planType.slice(1)} Plan`,
            },
            unit_amount: planLimits.price_cents,
            recurring: {
              interval: 'month',
            },
          },
          quantity: 1,
        },
      ],
      metadata: {
        userId: session.user.id,
        planType,
      },
      success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard/billing/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard/billing`,
    })

    return NextResponse.json({ checkoutUrl: checkoutSession.url })
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}

Multi-Tenant Testing Strategies

1. RLS Policy Testing

// tests/rls-policies.test.ts
import { createClient } from '@supabase/supabase-js'

describe('Row Level Security Policies', () => {
  let supabaseUser1: any
  let supabaseUser2: any
  let user1Id: string
  let user2Id: string

  beforeAll(async () => {
    // Create test users
    supabaseUser1 = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)
    supabaseUser2 = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)

    // Sign up test users
    const { data: user1 } = await supabaseUser1.auth.signUp({
      email: 'user1@test.com',
      password: 'testpassword123'
    })
    
    const { data: user2 } = await supabaseUser2.auth.signUp({
      email: 'user2@test.com',
      password: 'testpassword123'
    })

    user1Id = user1.user!.id
    user2Id = user2.user!.id
  })

  test('Users can only access their own campaigns', async () => {
    // User 1 creates a campaign
    const { data: campaign1 } = await supabaseUser1
      .from('campaigns')
      .insert({
        user_id: user1Id,
        name: 'User 1 Campaign',
        description: 'Test campaign'
      })
      .select()
      .single()

    // User 2 creates a campaign
    const { data: campaign2 } = await supabaseUser2
      .from('campaigns')
      .insert({
        user_id: user2Id,
        name: 'User 2 Campaign',
        description: 'Test campaign'
      })
      .select()
      .single()

    // User 1 should only see their own campaigns
    const { data: user1Campaigns } = await supabaseUser1
      .from('campaigns')
      .select('*')

    expect(user1Campaigns).toHaveLength(1)
    expect(user1Campaigns![0].user_id).toBe(user1Id)

    // User 2 should only see their own campaigns
    const { data: user2Campaigns } = await supabaseUser2
      .from('campaigns')
      .select('*')

    expect(user2Campaigns).toHaveLength(1)
    expect(user2Campaigns![0].user_id).toBe(user2Id)
  })

  test('Users cannot modify other users data', async () => {
    // User 1 tries to update User 2's campaign (should fail)
    const { error } = await supabaseUser1
      .from('campaigns')
      .update({ name: 'Hacked Campaign' })
      .eq('user_id', user2Id)

    expect(error).toBeDefined()
  })
})

2. Usage Limits Testing

// tests/usage-limits.test.ts
import { usageTracker } from '@/utils/usageTracking'

describe('Usage Limits Enforcement', () => {
  test('Trial users cannot exceed contact limits', async () => {
    const trialUserId = 'test-trial-user-id'
    
    // Mock trial user with 100 contact limit
    jest.spyOn(usageTracker, 'checkContactLimit').mockResolvedValue(false)

    const canAddContact = await usageTracker.checkContactLimit(trialUserId)
    expect(canAddContact).toBe(false)
  })

  test('Professional users have higher limits', async () => {
    const proUserId = 'test-pro-user-id'
    
    jest.spyOn(usageTracker, 'checkContactLimit').mockResolvedValue(true)

    const canAddContact = await usageTracker.checkContactLimit(proUserId)
    expect(canAddContact).toBe(true)
  })

  test('Usage is properly recorded', async () => {
    const userId = 'test-user-id'
    
    const recordUsageSpy = jest.spyOn(usageTracker, 'recordUsage')
    
    await usageTracker.recordUsage(userId, 'emails_sent', 1)
    
    expect(recordUsageSpy).toHaveBeenCalledWith(userId, 'emails_sent', 1)
  })
})

Implementation Integration with Framework

Framework Phase Integration

This guide integrates with the autonomous MVP framework phases as follows:

Phase 1: Pre-Build Assessment

  • Business Model Compliance: Validate multi-tenancy requirements against business model
  • Architecture Requirements: Ensure authentication and user isolation planned

Phase 1.5: Commercial SaaS Platform Validation

  • User Lifecycle Management: Self-signup, account management, subscription status
  • Legal Compliance: GDPR-compliant account deletion and data export

Phase 3: Design-First Feature Development

  • Multi-Tenant Database: Implement RLS policies and user isolation
  • Authentication Integration: Supabase Auth with Next.js middleware

Phase 4: Integration & Testing

  • Multi-Tenant Testing: RLS policy validation and user isolation testing
  • Usage Limits Testing: Subscription tier and billing validation

Critical Prevention Measures

This guide prevents the build-v1 multi-tenancy gaps by:

  1. Database Architecture: Proper RLS policies ensure user data isolation
  2. Authentication Flow: Complete signup-to-deletion user lifecycle
  3. Admin Management: Comprehensive admin tools for user and subscription management
  4. Business Integration: Usage tracking, billing, and subscription management
  5. Security Compliance: GDPR-compliant data handling and deletion

Success Criteria

A SaaS platform is properly multi-tenant when:

  • Multiple users can sign up and use the platform independently
  • User data is completely isolated at the database level
  • Usage is tracked and enforced according to subscription tiers
  • Admin tools allow comprehensive user and subscription management
  • Account deletion is GDPR-compliant with proper data export
  • All functionality works seamlessly across subscription tiers

This multi-tenancy development guide ensures SaaS platforms implement proper user isolation, account management, and business model integration from the start, preventing the single-user architecture gap that occurred in build-v1.