Back to Blog
bubble.iosupabasenext.jsmigrationreactpostgresqldatabase migrationno-code to code

Bubble to Supabase + Next.js: The Complete Migration Path

17 min read
By BubbleExport Team
Bubble to Supabase + Next.js: The Complete Migration Path

You've made the decision. The workload unit bills, the 8-second load times, the investor eyebrows—you've seen enough. You're ready to leave Bubble.

But here's the thing: "leaving Bubble" isn't a destination. It's a starting point. The real question is: leave Bubble for what?

After working with dozens of founders navigating this exact transition, one stack keeps emerging as the clear winner for most Bubble migrants:

Supabase + Next.js.

"We have moved out completely from bubble using supabase and next js. possibilities are endless" — munaeemmmm, Bubble Forum

That quote captures something important. Not just "we migrated"—but "possibilities are endless." The constraints that shaped your product decisions on Bubble? They evaporate.

"I never thought of going back to bubble" — munaeemmmm, months after migration

This isn't buyer's remorse talking. This is someone who discovered what building without artificial limits feels like.

This guide is the most comprehensive resource available for migrating from Bubble to Supabase + Next.js. We'll cover every layer: database, authentication, file storage, APIs, frontend, and deployment. By the end, you'll understand exactly what the migration involves and whether it's right for your app.

Why Supabase + Next.js? (The Migration-Friendly Stack)

Supabase and Next.js - The Migration-Friendly Stack

Before diving into the how, let's address the why. What makes this particular stack so well-suited for Bubble refugees?

Supabase: The Backend Bubble Founders Actually Understand

Supabase bills itself as "the open-source Firebase alternative," but for Bubble users, a better description might be: "what Bubble's backend should have been."

What Supabase gives you:

  • PostgreSQL database — Real, industry-standard SQL database you can query directly
  • Built-in authentication — Email/password, OAuth, magic links, just like Bubble but better
  • File storage — Upload, serve, and manage files with a proper CDN
  • Edge functions — Backend logic without managing servers (like Bubble workflows, but faster)
  • Real-time subscriptions — Live data updates without polling
  • Row-level security — Privacy rules, similar to Bubble's but more powerful
  • Dashboard — Visual interface for managing everything, like Bubble's data tab but better

The learning curve from Bubble to Supabase is gentler than you'd expect. You're still thinking in terms of users, data types, and access rules—just with industry-standard tools.

Pricing reality check:

  • Free tier: 500 MB database, 1 GB storage, 50,000 monthly active users
  • Pro ($25/month): 8 GB database, 100 GB storage, unlimited users
  • Scaling: $0.125 per GB-month for database, $0.021 per GB for storage

Compare that to Bubble's $29-$349/month plans with unpredictable workload charges. A $25/month Supabase project can handle what costs $200+/month on Bubble.

Next.js: The Frontend Framework Bubble Prepared You For

Next.js is a React framework that handles the complexity of modern web development. For Bubble users, the appeal is immediate:

  • Visual thinking still applies — Components in Next.js are like reusable elements in Bubble
  • Routing is intuitive — Pages live in a /pages or /app folder, just like they're named
  • No server config — Vercel deployment is as simple as Bubble's "deploy to live"
  • Performance by default — Server-side rendering, automatic code splitting, image optimization

Most importantly: Next.js is what companies hire developers to build. When you're recruiting talent, "we use Next.js" opens doors that "we use Bubble" closes.

"I've lost over 90% of potential clients because I couldn't offer code ownership." — Orbit, Bubble Forum

With Next.js, you own the code. Every line. Forever.

Understanding the Architecture Translation

Architecture Translation - Bubble to Supabase + Next.js

Before we migrate anything, let's understand how Bubble concepts map to Supabase + Next.js:

Bubble Concept Supabase + Next.js Equivalent
Data Types PostgreSQL Tables
Data Fields Table Columns
Option Sets Enum types or lookup tables
Current User Supabase Auth (useUser hook)
Privacy Rules Row Level Security (RLS) policies
Backend Workflows Edge Functions or API routes
Scheduled Workflows Cron triggers + Edge Functions
API Connector Direct HTTP calls or SDK
Plugins npm packages
Pages Next.js pages/routes
Reusable Elements React components
Custom States React state (useState)
URL Parameters Next.js router params
File Uploads Supabase Storage
Conditions/Filters SQL WHERE clauses

The concepts translate. The syntax changes.

Phase 1: Database Migration (Bubble → Supabase PostgreSQL)

Database Migration - Export, Schema, Relationships, Large Datasets

This is the foundation. Everything else depends on getting your data moved correctly.

Step 1: Export Your Bubble Data

In Bubble, go to Data → App Data and export each data type as CSV. Export:

  • All data types (Users, your custom types, etc.)
  • Include all fields
  • Export in UTF-8 encoding

Critical: Bubble's export includes internal IDs like _id. You'll need these to preserve relationships.

Step 2: Design Your PostgreSQL Schema

Bubble's database is "schemaless"—fields can hold anything. PostgreSQL requires explicit types. This is actually a feature: it catches errors before they reach users.

Example translation:

Bubble data type "Product":

  • Name (text)
  • Price (number)
  • Category (Option Set)
  • Created By (User)
  • Images (list of images)
  • Tags (list of texts)

PostgreSQL schema:

-- Create enum for categories (from your Option Set)
CREATE TYPE product_category AS ENUM ('electronics', 'clothing', 'food', 'other');

CREATE TABLE products (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  bubble_id TEXT UNIQUE,  -- Preserve original ID for migration
  name TEXT NOT NULL,
  price DECIMAL(10,2) NOT NULL DEFAULT 0,
  category product_category,
  created_by UUID REFERENCES auth.users(id),
  images TEXT[],  -- Array of image URLs
  tags TEXT[],    -- Array of strings
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Add index for common queries
CREATE INDEX idx_products_category ON products(category);
CREATE INDEX idx_products_created_by ON products(created_by);

Step 3: Handle Bubble's Relationship Model

This is where migrations get tricky. Bubble uses references between data types. PostgreSQL uses foreign keys.

The challenge:

"tables in Bubble works as Objects, because we relate each table with a reference to the other, not with single Ids like relational databases" — cdmunoz, Bubble Forum

The solution: Create a mapping table during migration:

-- Temporary mapping table
CREATE TABLE bubble_id_map (
  bubble_id TEXT PRIMARY KEY,
  table_name TEXT NOT NULL,
  new_id UUID NOT NULL
);

Migration script (Node.js example):

import { createClient } from '@supabase/supabase-js';
import { parse } from 'csv-parse/sync';
import fs from 'fs';

const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);

async function migrateProducts(csvPath) {
  const csv = fs.readFileSync(csvPath, 'utf-8');
  const records = parse(csv, { columns: true });
  
  for (const record of records) {
    // Look up the user's new ID from our mapping
    const createdById = await lookupNewId('users', record['Created By']);
    
    const { data, error } = await supabase
      .from('products')
      .insert({
        bubble_id: record._id,
        name: record.Name,
        price: parseFloat(record.Price) || 0,
        category: record.Category?.toLowerCase() || 'other',
        created_by: createdById,
        images: parseList(record.Images),
        tags: parseList(record.Tags),
        created_at: new Date(record['Created Date'])
      })
      .select('id')
      .single();
    
    if (error) {
      console.error('Failed to migrate:', record._id, error);
      continue;
    }
    
    // Store mapping for other tables that reference this product
    await supabase.from('bubble_id_map').insert({
      bubble_id: record._id,
      table_name: 'products',
      new_id: data.id
    });
  }
}

// Bubble lists are comma-separated or JSON
function parseList(value) {
  if (!value) return [];
  try {
    return JSON.parse(value);
  } catch {
    return value.split(',').map(s => s.trim()).filter(Boolean);
  }
}

async function lookupNewId(table, bubbleId) {
  if (!bubbleId) return null;
  const { data } = await supabase
    .from('bubble_id_map')
    .select('new_id')
    .eq('bubble_id', bubbleId)
    .eq('table_name', table)
    .single();
  return data?.new_id || null;
}

Step 4: Handle Large Datasets

"Migrating data out of Bubble is such a pain from someone who had to migrate over 3 million records" — stuart8, Bubble Forum

For large databases (100,000+ records):

  1. Export in batches — Bubble's export can timeout. Use Data API pagination
  2. Use COPY for bulk import — PostgreSQL's COPY is 10-100x faster than INSERT
  3. Disable indexes during import — Re-create them after for faster loading
  4. Run during off-hours — Both systems will be under load
-- Disable indexes during bulk import
ALTER INDEX idx_products_category DISABLE;

-- After import, re-enable
REINDEX INDEX idx_products_category;

Phase 2: Authentication Migration

Authentication Migration - User Accounts, Password Reset, Supabase Auth

Authentication is the most sensitive part of any migration. Here's how to handle it properly.

User Account Migration

What you can migrate:

  • Email addresses
  • Profile data (name, avatar, metadata)
  • Account creation dates
  • External OAuth provider IDs

What you cannot directly migrate:

  • Passwords (Bubble uses proprietary hashing)
  • Active sessions

The Password Reset Strategy

Since Bubble passwords can't be transferred, the most reliable approach is the transparent password reset:

// In your Next.js API route or auth handler
export async function POST(req) {
  const { email, password } = await req.json();
  
  const { data: user } = await supabase
    .from('users')
    .select('*')
    .eq('email', email)
    .single();
  
  if (!user) {
    return Response.json({ error: 'No account found' }, { status: 400 });
  }
  
  // Check if this is a migrated user who hasn't reset password
  if (user.migrated_from_bubble && !user.password_reset_completed) {
    // Trigger password reset
    await supabase.auth.resetPasswordForEmail(email, {
      redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/reset-password`
    });
    
    return Response.json({
      requiresReset: true,
      message: "We've upgraded our security. Check your email to set a new password."
    });
  }
  
  // Normal sign-in for users who have reset
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password
  });
  
  if (error) {
    return Response.json({ error: error.message }, { status: 400 });
  }
  
  return Response.json({ user: data.user });
}

Supabase Auth Setup

In your Supabase dashboard:

  1. Enable email provider — Settings → Authentication → Email
  2. Configure OAuth (if used) — Add Google, Apple, etc.
  3. Customize email templates — Auth → Templates
  4. Set redirect URLs — Include your production domain
// Client-side auth hook (Next.js)
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';

export function useAuth() {
  const supabase = createClientComponentClient();
  
  const signIn = async (email, password) => {
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password
    });
    return { data, error };
  };
  
  const signUp = async (email, password, metadata) => {
    const { data, error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        data: metadata // Store name, etc.
      }
    });
    return { data, error };
  };
  
  const signOut = async () => {
    await supabase.auth.signOut();
  };
  
  return { signIn, signUp, signOut };
}

Phase 3: File Storage Migration

File Storage Migration - Export URLs, Download/Re-upload, Update References

Bubble stores uploaded files on their CDN. You need to download them and re-upload to Supabase Storage.

Step 1: Export File URLs

Your data export includes file URLs like: https://s3.amazonaws.com/appforest_uf/...

Step 2: Download and Re-upload

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);

async function migrateFile(bubbleUrl, newPath) {
  // Download from Bubble CDN
  const response = await fetch(bubbleUrl);
  const blob = await response.blob();
  
  // Upload to Supabase Storage
  const { data, error } = await supabase.storage
    .from('uploads')
    .upload(newPath, blob, {
      contentType: response.headers.get('content-type'),
      upsert: true
    });
  
  if (error) {
    throw error;
  }
  
  // Return new public URL
  const { data: { publicUrl } } = supabase.storage
    .from('uploads')
    .getPublicUrl(newPath);
  
  return publicUrl;
}

// Usage
const newUrl = await migrateFile(
  'https://s3.amazonaws.com/appforest_uf/old-image.jpg',
  'products/product-123/image.jpg'
);

Step 3: Update Database References

After migrating files, update your database to point to new URLs:

UPDATE products
SET images = ARRAY(
  SELECT replace(url, 'old-cdn-domain', 'your-supabase-project.supabase.co')
  FROM unnest(images) AS url
)
WHERE images IS NOT NULL;

Phase 4: Backend Logic (Workflows → Edge Functions)

Backend Logic - Edge Functions vs API Routes, Workflow Conversion, Scheduled Tasks

Bubble backend workflows become Supabase Edge Functions or Next.js API routes.

Choosing Between Edge Functions and API Routes

Use Edge Functions when:

  • Logic needs to run close to users (low latency globally)
  • You want isolated, serverless functions
  • The function doesn't need Next.js-specific features

Use Next.js API Routes when:

  • Logic is tightly coupled to your frontend
  • You need server-side rendering context
  • You prefer keeping everything in one codebase

Example: Convert a Bubble Workflow

Bubble workflow: When a new order is created → calculate total → send confirmation email → update inventory

Edge Function equivalent:

// supabase/functions/process-order/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );
  
  const { orderId } = await req.json();
  
  // Fetch order with items
  const { data: order } = await supabase
    .from('orders')
    .select('*, items:order_items(*)')
    .eq('id', orderId)
    .single();
  
  // Calculate total
  const total = order.items.reduce((sum, item) => 
    sum + (item.price * item.quantity), 0
  );
  
  // Update order total
  await supabase
    .from('orders')
    .update({ total })
    .eq('id', orderId);
  
  // Update inventory
  for (const item of order.items) {
    await supabase.rpc('decrement_inventory', {
      product_id: item.product_id,
      amount: item.quantity
    });
  }
  
  // Send confirmation (using Resend, SendGrid, etc.)
  await sendOrderConfirmation(order);
  
  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' }
  });
});

Scheduled Workflows

Bubble's scheduled workflows become cron-triggered Edge Functions:

-- In Supabase, use pg_cron extension
SELECT cron.schedule(
  'daily-cleanup',
  '0 0 * * *',  -- Every day at midnight
  $$SELECT net.http_post(
    url := 'https://your-project.supabase.co/functions/v1/daily-cleanup',
    headers := '{"Authorization": "Bearer YOUR_ANON_KEY"}'::jsonb
  )$$
);

Phase 5: Frontend (Bubble Pages → Next.js)

Frontend Migration - Components, Data Fetching, Filters, Row Level Security

This is where the transformation feels most dramatic. You're moving from visual drag-and-drop to code.

Component-Based Thinking

Bubble's reusable elements are React components. The mental model transfers:

// components/ProductCard.jsx
export function ProductCard({ product }) {
  return (
    <div className="rounded-lg border p-4 shadow-sm">
      <img 
        src={product.images[0]} 
        alt={product.name}
        className="w-full h-48 object-cover rounded"
      />
      <h3 className="mt-2 font-semibold">{product.name}</h3>
      <p className="text-gray-600">${product.price}</p>
      <span className="text-sm text-gray-500">{product.category}</span>
    </div>
  );
}

Data Fetching

Bubble's "Do a search for" becomes Supabase queries:

// app/products/page.jsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { ProductCard } from '@/components/ProductCard';

export default async function ProductsPage() {
  const supabase = createServerComponentClient({ cookies });
  
  const { data: products } = await supabase
    .from('products')
    .select('*')
    .order('created_at', { ascending: false })
    .limit(20);
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-8">
      {products?.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Conditions and Filters

Bubble's "Search for Products where Category is Electronics" becomes:

const { data } = await supabase
  .from('products')
  .select('*')
  .eq('category', 'electronics')  // Where category equals
  .gte('price', 100)              // Where price >= 100
  .order('price', { ascending: true });

Privacy Rules → Row Level Security

Bubble's privacy rules become PostgreSQL RLS policies:

Bubble: "This Product is visible to Current User if Current User is Created By"

Supabase RLS:

-- Enable RLS
ALTER TABLE products ENABLE ROW LEVEL SECURITY;

-- Users can only see their own products
CREATE POLICY "Users see own products" ON products
  FOR SELECT
  USING (auth.uid() = created_by);

-- Users can only update their own products
CREATE POLICY "Users update own products" ON products
  FOR UPDATE
  USING (auth.uid() = created_by);

Phase 6: Deployment

Deployment - Vercel and Environment Variables

Bubble deployment was one click. With Next.js + Supabase, it's... also one click, just different clicks.

Vercel Deployment (Recommended)

  1. Push your code to GitHub
  2. Connect repository to Vercel
  3. Add environment variables (Supabase URL, keys)
  4. Deploy

Every push to main auto-deploys. Preview deployments for PRs. Instant rollbacks.

Vercel pricing:

  • Hobby (Free): Perfect for most migrated apps
  • Pro ($20/month/member): Team features, analytics
  • Enterprise: Custom

Environment Variables

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-key  # Server-side only

Post-Migration Checklist

Post-Migration Checklist - Database, Auth, Storage, Backend, Frontend, Performance

Before you flip the switch, verify everything:

Database:

  • All data types migrated
  • Relationships preserved
  • Unique constraints working
  • Indexes created for common queries

Authentication:

  • Sign up flow works
  • Sign in flow works
  • Password reset works
  • OAuth providers connected (if used)
  • Email templates customized

Storage:

  • All files migrated
  • Old URLs redirecting or replaced
  • Upload functionality working

Backend:

  • All workflows converted
  • Scheduled tasks running
  • API integrations working

Frontend:

  • All pages rendering
  • Data loading correctly
  • Forms submitting
  • Navigation working
  • Mobile responsive

Performance:

  • Page load times acceptable
  • No console errors
  • Images optimized

Realistic Timeline

Realistic Timeline - Simple, Medium, Complex, Enterprise

App Complexity Migration Duration
Simple (5-10 pages, basic data) 1-2 weeks
Medium (10-25 pages, complex data) 3-4 weeks
Complex (25+ pages, heavy workflows) 6-8 weeks
Enterprise (50+ pages, integrations) 8-12 weeks

These assume dedicated focus. Side-project migrations take 2-3x longer.

The Cost Difference (Real Numbers)

Cost Comparison - Bubble vs Supabase + Vercel

Let's make this concrete:

Scenario: 5,000 monthly active users, 2 GB database, 10 GB file storage

Bubble costs:

  • Professional plan: $149/month
  • Workload overages (typical): $50-150/month
  • Total: $200-300/month ($2,400-3,600/year)

Supabase + Vercel costs:

  • Supabase Pro: $25/month
  • Additional storage: ~$5/month
  • Vercel Pro (optional): $20/month
  • Total: $25-50/month ($300-600/year)

Annual savings: $1,800-3,300

A one-time migration investment of $2,500-5,000 pays for itself in 12-18 months. After that, you're saving thousands annually—forever.

Frequently Asked Questions

FAQ - Common Migration Questions

How long does it take to learn Supabase coming from Bubble?

Most Bubble founders become comfortable with Supabase basics within 1-2 weeks. The dashboard is intuitive, documentation is excellent, and concepts translate. The steeper learning curve is JavaScript/React for the frontend—budget 1-3 months to become proficient if you're starting from zero.

Can I migrate incrementally instead of all at once?

Yes. A common approach: keep Bubble for the frontend temporarily while migrating backend to Supabase. Use Bubble's API Connector to talk to your Supabase database. This lets you validate the data migration before rebuilding the UI.

What happens to my Bubble plugins?

Plugins become npm packages or custom code. Most common plugin functionality has standard JavaScript equivalents. However, highly Bubble-specific plugins may need custom implementation. Audit your plugin dependencies before migrating.

Do I need to know SQL?

Basic SQL helps significantly. You'll want to understand SELECT, INSERT, UPDATE, DELETE, and JOIN. Supabase's dashboard provides a visual query builder, but SQL knowledge unlocks advanced capabilities. Budget a few hours on SQLBolt or similar tutorials.

What about Bubble's visual design capabilities?

This is the biggest adjustment. You'll use Tailwind CSS, a component library like shadcn/ui, or a drag-and-drop React builder. The design freedom is actually greater—but the learning curve is real if you've never written CSS.

Can I hire someone to do this for me?

Yes. Migration services (like ours) handle the technical complexity while you focus on your business. The typical process: we export your data, rebuild your app in code, migrate your users, and hand you the keys. You end up with code you own, running on infrastructure you control.

The Path Forward

The Path Forward - Year 1, Year 2, Year 3+ Compound Benefits

Migrating from Bubble to Supabase + Next.js isn't just a technical project. It's a business decision with compound returns:

Year 1: Lower hosting costs, faster performance, code ownership Year 2: Easier hiring, investor confidence, feature velocity Year 3+: Compound advantages as custom code enables what no-code couldn't

"I never thought of going back to bubble" — munaeemmmm

That's not lock-in speaking. That's liberation.

The possibilities aren't just endless—they're yours.


Ready to migrate? Get a free migration assessment → We'll review your Bubble app, estimate the migration scope, and give you a realistic timeline and cost. If it doesn't make sense yet, we'll tell you.


Related reading:

Ready to talk migration?

Get a free assessment of your Bubble app. We'll tell you exactly what to expect — timeline, cost, and any potential challenges.

View Pricing
AB
Made byAbhi