This document outlines the complete migration strategy from Spring Boot + PostgreSQL to Supabase + Vercel while maintaining all existing functionality.
- Backend: Migrate from Spring Boot to Supabase Edge Functions
- Database: Migrate from local PostgreSQL to Supabase PostgreSQL
- Authentication: Replace JWT with Supabase Auth
- Frontend: Update React app to use Supabase client
- Deployment: Deploy to Vercel with optimized configuration
- Performance: Maintain or improve current performance
- Data Integrity: Ensure zero data loss during migration
- Framework: Spring Boot 3.x with Java
- Database: PostgreSQL with JPA/Hibernate
- Authentication: JWT-based with custom implementation
- API Endpoints: RESTful APIs for auth, providers, services, categories, admin
- File Upload: Spring Boot multipart file handling
- Security: Spring Security with CORS configuration
- Framework: React 18 with Material UI
- State Management: React hooks and context
- API Client: Axios with interceptors
- Authentication: JWT token storage and refresh
- Routing: React Router DOM
-- Core Tables
- users (21 records)
- roles (3 records)
- service_providers (20 records)
- service_categories (10 records)
- services (40 records)
- bookings (3 records)
- reviews (2 records)- Edge Functions: TypeScript/Deno functions for business logic
- Database: Supabase PostgreSQL with Row Level Security
- Authentication: Supabase Auth with social providers
- Storage: Supabase Storage for file uploads
- Real-time: Supabase subscriptions for live updates
- Deployment: Vercel with automatic deployments
- Environment: Optimized build configuration
- CDN: Global edge network for performance
- Analytics: Vercel Analytics integration
- Create Supabase project
- Set up database schema
- Configure authentication
- Set up development environment
- Export existing data
- Create Supabase tables
- Import data with transformations
- Set up Row Level Security policies
- Create Edge Functions
- Implement business logic
- Set up file storage
- Configure CORS and security
- Install Supabase client
- Update authentication flow
- Migrate API calls
- Implement real-time features
- Configure Vercel deployment
- Set up environment variables
- Comprehensive testing
- Performance optimization
- Update documentation
- Create deployment guides
- Final validation
- Go-live preparation
-- Supabase users table (extends auth.users)
CREATE TABLE public.user_profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
phone_number TEXT,
address TEXT,
profile_image_url TEXT,
date_of_birth TIMESTAMPTZ,
gender TEXT CHECK (gender IN ('MALE', 'FEMALE', 'OTHER', 'PREFER_NOT_TO_SAY')),
is_active BOOLEAN DEFAULT true,
is_verified BOOLEAN DEFAULT false,
email_verified BOOLEAN DEFAULT false,
phone_verified BOOLEAN DEFAULT false,
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);-- Roles as enum or separate table
CREATE TYPE user_role AS ENUM ('USER', 'PROVIDER', 'ADMIN');
ALTER TABLE public.user_profiles
ADD COLUMN role user_role DEFAULT 'USER';CREATE TABLE public.service_providers (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES public.user_profiles(id) UNIQUE NOT NULL,
business_name TEXT,
description TEXT,
years_of_experience INTEGER,
specializations TEXT,
service_area TEXT,
average_rating DECIMAL(3,2) DEFAULT 0.0,
total_reviews INTEGER DEFAULT 0,
is_verified BOOLEAN DEFAULT false,
is_available BOOLEAN DEFAULT true,
profile_image_url TEXT,
license_number TEXT,
insurance_details TEXT,
website_url TEXT,
social_media_links JSONB,
emergency_contact TEXT,
business_hours JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);-- Migration script to transform Spring Boot data to Supabase format
-- This will be executed after schema creation-- User profiles RLS
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own profile" ON public.user_profiles
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update own profile" ON public.user_profiles
FOR UPDATE USING (auth.uid() = id);
-- Service providers RLS
ALTER TABLE public.service_providers ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view active providers" ON public.service_providers
FOR SELECT USING (is_available = true);
CREATE POLICY "Providers can update own profile" ON public.service_providers
FOR UPDATE USING (auth.uid() = user_id);// supabase/functions/auth-helpers/index.ts
import { createClient } from '@supabase/supabase-js'
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
export async function getUserProfile(supabase: any, userId: string) {
const { data, error } = await supabase
.from('user_profiles')
.select('*')
.eq('id', userId)
.single()
if (error) throw error
return data
}// supabase/functions/providers/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from '@supabase/supabase-js'
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
)
const { method, url } = req
const urlPath = new URL(url).pathname
switch (method) {
case 'GET':
if (urlPath.includes('/search')) {
return await searchProviders(req, supabase)
}
return await getProviders(req, supabase)
case 'POST':
return await createProvider(req, supabase)
case 'PUT':
return await updateProvider(req, supabase)
default:
return new Response('Method not allowed', { status: 405 })
}
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
})// supabase/functions/services/index.ts
// Similar structure for service CRUD operations// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
}
})// services/authService.ts
import { supabase } from '../lib/supabase'
export const authService = {
async signUp(email: string, password: string, userData: any) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: userData
}
})
if (error) throw error
// Create user profile
if (data.user) {
await supabase
.from('user_profiles')
.insert({
id: data.user.id,
...userData
})
}
return data
},
async signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
},
async signOut() {
const { error } = await supabase.auth.signOut()
if (error) throw error
},
async getCurrentUser() {
const { data: { user } } = await supabase.auth.getUser()
if (user) {
const { data: profile } = await supabase
.from('user_profiles')
.select('*')
.eq('id', user.id)
.single()
return { ...user, profile }
}
return null
}
}{
"dependencies": {
"@supabase/supabase-js": "^2.38.0",
"@supabase/auth-helpers-react": "^0.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}// contexts/AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react'
import { User, Session } from '@supabase/supabase-js'
import { supabase } from '../lib/supabase'
interface AuthContextType {
user: User | null
session: Session | null
loading: boolean
signIn: (email: string, password: string) => Promise<any>
signUp: (email: string, password: string, userData: any) => Promise<any>
signOut: () => Promise<void>
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType)
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null)
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
setUser(session?.user ?? null)
setLoading(false)
})
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
setSession(session)
setUser(session?.user ?? null)
setLoading(false)
}
)
return () => subscription.unsubscribe()
}, [])
const value = {
user,
session,
loading,
signIn: authService.signIn,
signUp: authService.signUp,
signOut: authService.signOut
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}// services/providerService.ts
import { supabase } from '../lib/supabase'
export const providerService = {
async getProviders(filters = {}) {
let query = supabase
.from('service_providers')
.select(`
*,
user_profiles(*),
services(*)
`)
.eq('is_available', true)
// Apply filters
if (filters.category) {
query = query.eq('category_id', filters.category)
}
const { data, error } = await query
if (error) throw error
return data
},
async getProvider(id: string) {
const { data, error } = await supabase
.from('service_providers')
.select(`
*,
user_profiles(*),
services(*),
reviews(*)
`)
.eq('id', id)
.single()
if (error) throw error
return data
},
async createProvider(providerData: any) {
const { data, error } = await supabase
.from('service_providers')
.insert(providerData)
.select()
.single()
if (error) throw error
return data
}
}{
"version": 2,
"builds": [
{
"src": "package.json",
"use": "@vercel/static-build",
"config": {
"distDir": "build"
}
}
],
"routes": [
{
"src": "/static/(.*)",
"headers": {
"cache-control": "s-maxage=31536000,immutable"
}
},
{
"src": "/(.*)",
"dest": "/index.html"
}
],
"env": {
"REACT_APP_SUPABASE_URL": "@supabase_url",
"REACT_APP_SUPABASE_ANON_KEY": "@supabase_anon_key"
}
}# Vercel Environment Variables
REACT_APP_SUPABASE_URL=https://your-project.supabase.co
REACT_APP_SUPABASE_ANON_KEY=your-anon-key
REACT_APP_APP_VERSION=2.0.0
REACT_APP_ENVIRONMENT=production{
"scripts": {
"build": "react-scripts build",
"build:analyze": "npm run build && npx bundle-analyzer build/static/js/*.js",
"build:prod": "GENERATE_SOURCEMAP=false npm run build"
}
}City-Service/
βββ supabase/
β βββ functions/
β β βββ auth-helpers/
β β βββ providers/
β β βββ services/
β β βββ categories/
β β βββ admin/
β βββ migrations/
β βββ config.toml
βββ frontend/
β βββ src/
β β βββ lib/
β β β βββ supabase.ts
β β βββ contexts/
β β β βββ AuthContext.tsx
β β βββ services/
β β β βββ authService.ts
β β β βββ providerService.ts
β β β βββ serviceService.ts
β β βββ components/
β βββ vercel.json
β βββ package.json
βββ migration/
βββ data-export.sql
βββ schema-migration.sql
βββ data-import.sql
// hooks/useRealtimeProviders.ts
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'
export const useRealtimeProviders = () => {
const [providers, setProviders] = useState([])
useEffect(() => {
// Initial fetch
fetchProviders()
// Subscribe to changes
const subscription = supabase
.channel('providers')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'service_providers' },
(payload) => {
handleRealtimeUpdate(payload)
}
)
.subscribe()
return () => {
subscription.unsubscribe()
}
}, [])
const fetchProviders = async () => {
const { data } = await supabase
.from('service_providers')
.select('*')
setProviders(data || [])
}
const handleRealtimeUpdate = (payload) => {
switch (payload.eventType) {
case 'INSERT':
setProviders(prev => [...prev, payload.new])
break
case 'UPDATE':
setProviders(prev => prev.map(p =>
p.id === payload.new.id ? payload.new : p
))
break
case 'DELETE':
setProviders(prev => prev.filter(p => p.id !== payload.old.id))
break
}
}
return { providers }
}- Data integrity validation
- API endpoint compatibility
- Authentication flow testing
- Performance benchmarking
- Security audit
// tests/e2e/auth.test.ts
import { test, expect } from '@playwright/test'
test('user can sign up and sign in', async ({ page }) => {
await page.goto('/register')
// Fill registration form
await page.fill('[data-testid="email"]', 'test@example.com')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="register-button"]')
// Verify redirect to dashboard
await expect(page).toHaveURL('/dashboard')
})- Proper indexing on frequently queried columns
- Connection pooling configuration
- Query optimization with EXPLAIN ANALYZE
- Code splitting with React.lazy()
- Image optimization with Vercel
- Bundle size monitoring
- CDN utilization
- Supabase query caching
- Browser caching headers
- Service worker implementation
- Comprehensive RLS policies
- User data isolation
- Admin access controls
- Rate limiting on Edge Functions
- Input validation and sanitization
- CORS configuration
- Multi-factor authentication setup
- Session management
- Password policies
- Backup existing database
- Set up Supabase project
- Create development environment
- Test data export/import process
- Execute database migration
- Deploy Edge Functions
- Update frontend code
- Configure Vercel deployment
- Update DNS settings
- Verify all functionality
- Performance testing
- Security audit
- User acceptance testing
- Documentation updates
- DNS Revert: Point domain back to original server
- Database Restore: Restore from pre-migration backup
- Code Revert: Revert to previous Git commit
- Service Restart: Restart original Spring Boot application
- Ability to rollback individual components
- Feature flags for gradual migration
- A/B testing capabilities
- Supabase dashboard monitoring
- Vercel analytics
- Error tracking with Sentry
- Performance monitoring
- Regular database maintenance
- Security updates
- Performance optimization
- Backup verification
- Free Tier: Up to 500MB database, 2GB bandwidth
- Pro Tier: $25/month for production use
- Additional: Storage and bandwidth overages
- Hobby: Free for personal projects
- Pro: $20/month for team collaboration
- Enterprise: Custom pricing
- Development time: ~12 days
- Testing and validation: ~3 days
- Documentation: ~1 day
- Total Estimated: 16 development days
This migration plan provides a comprehensive roadmap for transitioning from Spring Boot + PostgreSQL to Supabase + Vercel while maintaining all existing functionality and improving scalability, performance, and developer experience.