This guide walks you through executing the comprehensive database schema redesign that transforms MoveLink from a quote generator to a full two-sided marketplace platform.
- movers - Mover profiles, verification, availability
- bookings - Customer booking requests and job tracking
- payments - Payment transactions, escrow, commissions
- ratings - Two-way ratings between customers and movers
- mover_locations - Real-time GPS tracking
- mover_availability_schedule - Mover working hours
- booking_requests - Pending mover assignments
- notifications - In-app and push notifications
verification_status- Mover verification statesavailability_status- Online/offline/busybooking_status- Booking lifecycle statespayment_status- Payment processing statespayment_method- M-Pesa, card, cash, etc.vehicle_type- Types of moving vehicles
find_nearby_movers()- Proximity searchcalculate_distance_km()- Distance calculationsis_location_in_service_area()- Service area checkingget_mover_stats()- Performance analyticsget_platform_stats()- Business intelligence
- Comprehensive policies for all tables
- Customer/mover/admin role separation
- Secure data access controls
Before making any changes, create a backup:
# In Supabase Dashboard
1. Go to Database → Backups
2. Click "Create Backup"
3. Wait for completion
4. Note the backup timestampOr via CLI:
supabase db dump > backup_$(date +%Y%m%d_%H%M%S).sqlWhy check first? If PostGIS is not enabled, all migrations will fail with geography/geometry type errors. Checking now saves time and prevents partial migration failures.
-- Run this in SQL Editor
SELECT
extname AS extension_name,
extversion AS version,
'PostGIS is enabled ✓' AS status
FROM pg_extension
WHERE extname = 'postgis';Expected Result:
extension_name | version | status
postgis | 3.x.x | PostGIS is enabled ✓
-- Enable PostGIS extension
CREATE EXTENSION IF NOT EXISTS postgis;
-- Verify it was enabled
SELECT extname, extversion FROM pg_extension WHERE extname = 'postgis';After enabling, you should see:
postgisextension listed- Version 3.0 or higher recommended
- Error: "permission denied" → Contact your Supabase admin or check project permissions
- Extension not available → PostGIS should be available by default in Supabase; contact support
- Version < 3.0 → Still usable, but update recommended for better performance
✅ Checkpoint: Only proceed to Step 3 if PostGIS is confirmed enabled.
- Open your Supabase project dashboard at
https://supabase.com/dashboard - Select your project from the project list
- In the left sidebar, look for the SQL Editor icon (it looks like a database or terminal icon)
- Click on SQL Editor to open the SQL query interface
- You should see:
- A query editor panel in the center
- A "Run" button (or use Ctrl+Enter / Cmd+Enter to execute)
- Optional: A history of previous queries on the left
Tip: The SQL Editor is typically located below "Database" and above "Functions" in the sidebar menu.
File: supabase/migrations/20251008_part1_marketplace_schema.sql
- Open the file in your code editor
- Copy the entire contents
- Paste into Supabase SQL Editor
- Click "Run" button (or press Ctrl+Enter)
- Wait for completion (should take ~10-15 seconds)
Expected Output:
Success. No rows returned
Verify Part 1 Completion:
-- Run this query to verify tables were created
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('movers', 'bookings');Expected Result:
table_name
------------
movers
bookings
File: supabase/migrations/20251008_part2_marketplace_schema.sql
- Copy the entire contents of Part 2
- Paste into SQL Editor (clear previous query first)
- Click "Run"
- Wait for completion (~10 seconds)
Verify Part 2 Completion:
-- Check all new tables
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;Expected Tables:
- bookings
- booking_requests
- mover_availability_schedule
- mover_locations
- movers
- notifications
- payments
- quotes (existing)
- profiles (existing)
- ratings
File: supabase/migrations/20251008_part3_rls_and_functions.sql
- Copy Part 3 contents
- Paste into SQL Editor
- Click "Run"
- Wait for completion (~15-20 seconds)
Verify Part 3 Completion:
Check RLS is enabled:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN ('movers', 'bookings', 'payments');Expected Result:
tablename | rowsecurity
-------------+-------------
movers | t
bookings | t
payments | t
Check functions were created:
SELECT routine_name
FROM information_schema.routines
WHERE routine_schema = 'public'
AND routine_name LIKE '%mover%';Expected Functions:
- find_nearby_movers
- get_mover_stats
- is_location_in_service_area
- update_mover_rating
- update_mover_current_location
- etc.
✅ Note: PostGIS extension verification was completed in Step 2. If you skipped that step, go back and verify PostGIS is enabled before proceeding.
All geospatial functions (geography, geometry types, ST_* functions) should now be available.
Create test data to verify everything works.
auth.uid() function returns NULL when run from the SQL Editor because the SQL session is not authenticated through Supabase Auth. You must retrieve a valid user ID first.
Run this query to find an existing user:
-- Get the first user from your auth.users table
SELECT id, email
FROM auth.users
LIMIT 1;Copy the id value (it will look like: a1b2c3d4-e5f6-7890-abcd-ef1234567890)
If no users exist, you need to create one through your application's sign-up flow first, or create one manually:
-- Optional: Create a test user (if none exist)
-- You'll need to use Supabase Auth API or your app's sign-up for proper user creationReplace 'YOUR_USER_ID_HERE' with the actual user ID you copied above:
-- Insert a test mover
INSERT INTO public.movers (
user_id,
business_name,
phone_primary,
vehicle_types,
service_radius_km,
verification_status
) VALUES (
'YOUR_USER_ID_HERE'::uuid, -- Replace with actual user ID from auth.users
'Test Movers Kenya',
'+254712345678',
ARRAY['pickup', 'box_truck_small']::vehicle_type[],
15,
'verified'
);
-- Verify insertion
SELECT id, business_name, verification_status, user_id
FROM public.movers
WHERE business_name = 'Test Movers Kenya';Alternative: If you want to use auth.uid() dynamically (though it will be NULL in SQL Editor):
-- This uses COALESCE to provide a fallback
DO $$
DECLARE
v_user_id uuid;
BEGIN
-- Try to get current auth user, fallback to first user in system
v_user_id := COALESCE(
auth.uid(),
(SELECT id FROM auth.users LIMIT 1)
);
-- Insert with the resolved user_id
INSERT INTO public.movers (
user_id,
business_name,
phone_primary,
vehicle_types,
service_radius_km,
verification_status
) VALUES (
SELECT COUNT(*) as function_count
FROM information_schema.routines
WHERE routine_schema = 'public'
AND (routine_name LIKE '%mover%' OR routine_name LIKE '%booking%');
'Test Movers Kenya',
'+254712345678',
ARRAY['pickup', 'box_truck_small']::vehicle_type[],
15,
'verified'
);
RAISE NOTICE 'Inserted test mover with user_id: %', v_user_id;
END $$;
-- Verify insertion
SELECT id, business_name, verification_status, user_id
FROM public.movers
WHERE business_name = 'Test Movers Kenya';After completing all migrations, verify:
SELECT COUNT(*) as table_count
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN (
'movers', 'bookings', 'payments', 'ratings',
'mover_locations', 'mover_availability_schedule',
'booking_requests', 'notifications'
);Expected: table_count = 8
SELECT typname FROM pg_type
WHERE typname IN (
'verification_status', 'availability_status',
'booking_status', 'payment_status'
);Expected: 4+ enum types
SELECT COUNT(*) as index_count
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename IN ('movers', 'bookings', 'payments');Expected: 30+ indexes
SELECT COUNT(*) as policy_count
FROM pg_policies
WHERE schemaname = 'public';Expected: 25+ policies
SELECT COUNT(*) as function_count
FROM information_schema.routines
WHERE routine_schema = 'public'
AND routine_name LIKE '%mover%' OR routine_name LIKE '%booking%';Expected: 10+ functions
-- Check existing tables SELECT tablename FROM pg_tables WHERE schemaname = 'public';
-- WARNING: DROP CASCADE will permanently delete the table, all data, -- and ALL dependent objects (foreign keys, views, triggers, etc.) -- This cannot be undone without a backup! -- Only run this if you're certain you want to start fresh: DROP TABLE IF EXISTS public.movers CASCADE; -- Then re-run migration
-- Drop and recreate
DROP TYPE IF EXISTS verification_status CASCADE;
-- Then re-run migrationSolution: PostGIS not installed.
CREATE EXTENSION IF NOT EXISTS postgis;Solution: You may not have admin access.
- Verify you're logged in as project owner
- Check Supabase dashboard for proper permissions
Solution: Large migration may take time.
- Wait 30-60 seconds
- Check Supabase dashboard for any error notifications
- Review Supabase logs: Database → Logs
INSERT INTO public.movers (
user_id, business_name, phone_primary,
vehicle_types, verification_status
) VALUES (
'your-user-id-here',
'Test Moving Company',
'+254700000000',
ARRAY['pickup']::vehicle_type[],
'verified'
) RETURNING *;-- Find movers near Nairobi CBD
SELECT * FROM find_nearby_movers(
ST_GeographyFromText('POINT(36.8219 -1.2921)'),
20, -- 20km radius
3.0 -- min rating 3.0
);INSERT INTO public.bookings (
customer_id,
pickup_address,
pickup_location,
dropoff_address,
dropoff_location,
scheduled_date,
scheduled_time_start,
property_size,
estimated_price
) VALUES (
'your-user-id',
'Westlands, Nairobi',
ST_GeographyFromText('POINT(36.8095 -1.2672)'),
'Kilimani, Nairobi',
ST_GeographyFromText('POINT(36.7871 -1.2944)'),
CURRENT_DATE + INTERVAL '7 days',
'09:00:00',
'2BR',
25000.00
) RETURNING *;-- As a customer, try to view all movers (should only see verified ones)
SELECT COUNT(*) FROM public.movers WHERE verification_status = 'verified';
-- Try to update another user's mover profile (should fail)
UPDATE public.movers
SET business_name = 'Hacked!'
WHERE user_id != auth.uid();
-- Expected: 0 rows updated (RLS blocking)If something goes wrong, restore from backup:
- Go to Database → Backups
- Find your pre-migration backup
- Click "Restore"
- Confirm restoration
/*
╔═══════════════════════════════════════════════════════════════════════════╗
║ ║
║ ⚠️ CRITICAL WARNING - READ THIS FIRST ⚠️ ║
║ ║
║ THE FOLLOWING OPERATIONS ARE IRREVERSIBLE AND WILL PERMANENTLY DELETE: ║
║ ║
║ • ALL TABLES (movers, bookings, payments, ratings, etc.) ║
║ • ALL DATA stored in those tables ║
║ • ALL FOREIGN KEY RELATIONSHIPS ║
║ • ALL INDEXES, TRIGGERS, AND CONSTRAINTS ║
║ • ALL ROW-LEVEL SECURITY POLICIES ║
║ ║
║ ⛔ MANDATORY STEPS BEFORE PROCEEDING: ║
║ ║
║ 1. CREATE A FULL DATABASE BACKUP ║
║ → Supabase Dashboard > Database > Backups > Create Backup ║
║ ║
║ 2. VERIFY YOU ARE IN THE CORRECT ENVIRONMENT ║
║ → Run: SELECT current_database(); ║
║ → CONFIRM this is NOT your production database ║
║ ║
║ 3. EXPORT CRITICAL DATA (if needed for recovery) ║
║ → Run: COPY (SELECT * FROM public.movers) TO '/tmp/movers.csv'; ║
║ → Repeat for other tables you need to preserve ║
║ ║
║ 4. REQUIRED CONFIRMATION - Type this command first: ║
║ → SELECT 'I confirm this is NOT production and I have a backup' ║
║ AS confirmation; ║
║ ║
║ 5. ONLY AFTER COMPLETING STEPS 1-4, uncomment and run the DROP commands ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════╝
*/
-- STEP 1: VERIFY ENVIRONMENT (REQUIRED)
SELECT
current_database() AS database_name,
current_user AS current_user,
version() AS postgres_version;
-- STEP 2: CONFIRMATION (REQUIRED - READ THE WARNING ABOVE)
SELECT 'I confirm this is NOT production and I have a backup' AS confirmation;
-- STEP 3: UNCOMMENT THE FOLLOWING LINES TO EXECUTE ROLLBACK
-- ⚠️ WARNING: Remove the '--' comment prefix ONLY after completing verification steps
-- Drop all new tables (CASCADE will delete dependent objects)
-- DROP TABLE IF EXISTS public.notifications CASCADE;
-- DROP TABLE IF EXISTS public.booking_requests CASCADE;
-- DROP TABLE IF EXISTS public.mover_availability_schedule CASCADE;
-- DROP TABLE IF EXISTS public.mover_locations CASCADE;
-- DROP TABLE IF EXISTS public.ratings CASCADE;
-- DROP TABLE IF EXISTS public.payments CASCADE;
-- DROP TABLE IF EXISTS public.bookings CASCADE;
-- DROP TABLE IF EXISTS public.movers CASCADE;
-- Drop enums (CASCADE will handle dependencies)
-- DROP TYPE IF EXISTS verification_status CASCADE;
-- DROP TYPE IF EXISTS availability_status CASCADE;
-- DROP TYPE IF EXISTS booking_status CASCADE;
-- DROP TYPE IF EXISTS payment_status CASCADE;
-- DROP TYPE IF EXISTS payment_method CASCADE;
-- DROP TYPE IF EXISTS vehicle_type CASCADE;
-- DROP TYPE IF EXISTS rating_type CASCADE;
-- STEP 4: VERIFY ROLLBACK COMPLETION
-- SELECT COUNT(*) as remaining_tables
-- FROM information_schema.tables
-- WHERE table_schema = 'public'
-- AND table_name IN (
-- 'movers', 'bookings', 'payments', 'ratings',
-- 'mover_locations', 'mover_availability_schedule',
-- 'booking_requests', 'notifications'
-- );
-- Expected: remaining_tables = 0
/*
╔═══════════════════════════════════════════════════════════════════════════╗
║ ✅ IF ROLLBACK SUCCESSFUL: ║
║ • All tables should be removed ║
║ • All data is permanently deleted ║
║ • You can now restore from backup or re-run migrations ║
║ ║
║ ⛔ IF YOU MADE A MISTAKE: ║
║ • STOP IMMEDIATELY ║
║ • Restore from your backup (Option 1 above) ║
║ • Contact your database administrator if unsure ║
╚═══════════════════════════════════════════════════════════════════════════╝
*/-
Update Application Code
- Update TypeScript types to match new schema
- Implement mover registration flow
- Build booking creation components
- Add real-time location tracking
-
Test All User Flows
- Customer registration → booking creation
- Mover registration → profile verification
- Booking acceptance → job completion
- Payment processing → payout
-
Populate Initial Data
- Add verified movers (seed data)
- Create test bookings
- Set up mover availability schedules
-
Monitor Performance
- Check query performance with
EXPLAIN ANALYZE - Monitor index usage
- Optimize slow queries
- Check query performance with
- Run migrations during low traffic to avoid disruptions
- Test in development environment first before production
- Keep the SQL files in version control for future reference
- Document any custom changes you make to the schema
- Set up database monitoring to track performance post-migration
You'll know the migration was successful when:
- All 8 tables are created and visible in Database → Tables
- Sample queries return data without errors
- RLS policies block unauthorized access appropriately
- Geospatial functions work (find_nearby_movers returns results)
- No error messages in Supabase logs
Congratulations! Your database is now ready for a full marketplace platform! 🎉
If you encounter issues:
- Check Supabase dashboard logs: Database → Logs
- Review this guide's troubleshooting section
- Test queries one at a time to isolate errors
- Verify your Supabase project has sufficient permissions
- Check Supabase status page for any ongoing issues
Document Version: 1.0
Last Updated: October 8, 2025
Compatible With: Supabase PostgreSQL 15+