Skip to content
Base44 SEO Journal

Migrating a Base44 app to Supabase, step by step

Move a Base44 app to Supabase: turn entities into tables, import the data, move users, replace Base44 SDK calls, and port backend functions and integrations.

14 min read

Moving a Base44 app to Supabase means rebuilding the backend your frontend talks to. The frontend stays: it’s standard React. You turn each Base44 entity into a Postgres table, import your CSV data, move your users, then replace every Base44 SDK call with its Supabase equivalent. Backend functions port well, because both platforms run them on Deno. The size of the job is the number of SDK calls in your code.

This is level 4 of the Base44 migration guide. Do levels 1 and 2 first: you need the code and the data exported.

Before you start: size the job

Export the code (here’s how) and count what you’ll replace:

grep -rhoE 'base44\.(entities\.[A-Za-z]+\.[a-zA-Z]+|auth\.[a-zA-Z]+|integrations\.Core\.[A-Za-z]+|functions\.[a-zA-Z]+|agents\.[a-zA-Z]+)' src \
  | sort | uniq -c | sort -rn

Some generated apps import helpers instead of calling base44.* directly, for example import { Product } from '@/api/entities' or import { SendEmail } from '@/api/integrations'. Check src/api/ and search for those imports too.

Make a list: entities and their operations, auth calls, integrations, functions, agents. That list is your migration plan.

Step 1: create the Supabase project and tables

Each entity has a JSON schema defining its fields. Get them with base44 eject (they land in the project’s entities folder) or read them in the Data page. A typical schema translates like this:

Base44 field typePostgres column
stringtext
string with format: date / date-timedate / timestamptz
number / integernumeric / integer
booleanboolean
array, objectjsonb (or a separate table if you query into it)
enumtext with a check constraint
Reference to another entitytext column holding the other record’s ID

Base44 adds built-in fields to every record: an id, created_date, updated_date and created_by. Keep them.

create table public.product (
  id            text primary key,              -- keep Base44's IDs so URLs survive
  name          text not null,
  price         numeric,
  status        text check (status in ('draft', 'published')),
  tags          jsonb default '[]'::jsonb,
  created_date  timestamptz default now(),
  updated_date  timestamptz default now(),
  created_by    text
);
alter table public.product enable row level security;

Step 2: import the data

Export each table to CSV from Base44’s Data page (details), then import it in Supabase’s table editor or with psql’s \copy. Import tables that others reference first. Check row counts on both sides before moving on.

Array and object fields arrive as JSON text in the CSV. If Supabase’s importer doesn’t parse them into jsonb, import them into a text column and convert with alter table ... alter column ... type jsonb using col::jsonb.

Step 3: move your users

Export the User table from the Data page. Base44 documents no way to export password hashes, so you can’t carry passwords over. Pick one:

  • Import users and send reset links. Create each user in Supabase Auth with the admin API, then send a password reset email.
  • Switch to magic links or social login. Users sign in with their email address or Google account, and nobody needs a password reset.

Keep a mapping from each user’s old Base44 ID to their new Supabase user ID, and update created_by and any user-reference columns with it.

Step 4: replace the SDK calls

This is most of the work. The common entity operations map directly:

Base44 SDKSupabase JS
base44.entities.Product.list('-created_date', 10)supabase.from('product').select('*').order('created_date', { ascending: false }).limit(10)
base44.entities.Product.filter({ status: 'published' })supabase.from('product').select('*').eq('status', 'published')
base44.entities.Product.get(id)supabase.from('product').select('*').eq('id', id).single()
base44.entities.Product.create(data)supabase.from('product').insert(data).select().single()
base44.entities.Product.update(id, data)supabase.from('product').update(data).eq('id', id)
base44.entities.Product.delete(id)supabase.from('product').delete().eq('id', id)
base44.auth.me()supabase.auth.getUser()
base44.auth.logout()supabase.auth.signOut()
base44.auth.loginWithProvider('google')supabase.auth.signInWithOAuth({ provider: 'google' })
base44.integrations.Core.UploadFile(...)supabase.storage.from('uploads').upload(path, file)

Supabase returns { data, error } where Base44 returns the records directly, so each call site needs a small change in how it reads the result. A thin wrapper module with the same method names as your Base44 entities keeps the rest of the code untouched:

// src/api/db.js
import { supabase } from './supabase'

const table = (name) => ({
  list: async (order = '-created_date', limit = 50) => {
    const col = order.replace(/^-/, '')
    const { data, error } = await supabase.from(name).select('*')
      .order(col, { ascending: !order.startsWith('-') }).limit(limit)
    if (error) throw error
    return data
  },
  filter: async (where) => {
    const { data, error } = await supabase.from(name).select('*').match(where)
    if (error) throw error
    return data
  },
  get: async (id) => {
    const { data, error } = await supabase.from(name).select('*').eq('id', id).single()
    if (error) throw error
    return data
  },
})

export const Product = table('product')

Base44’s filter() also accepts MongoDB-style operators such as $in and $gte. Those need translating by hand (.in(), .gte()), so search for $ inside filter calls.

Step 5: replace permissions with row level security

Base44 entities have security rules that decide who can read and write each record. In Supabase that’s row level security. Recreate each rule as a policy, for example “users can read their own records”:

create policy "own rows" on public.product
  for select using (created_by = auth.uid()::text);

Test every policy by signing in as an ordinary user. A table with RLS enabled and no policies returns nothing, which is safe but confusing. A table without RLS is open to anyone with your public key.

Step 6: port backend functions and integrations

Base44 backend functions run on Deno, and so do Supabase Edge Functions, so the logic usually ports with two changes: read data through a Supabase client instead of createClientFromRequest, and read secrets from Deno.env.get() instead of base44:runtime.

Built-in integrations need a provider of your own:

Base44 built-inReplacement
SendEmailResend, Postmark or SES, called from an Edge Function
InvokeLLMOpenAI, Anthropic or another API, called from an Edge Function
UploadFile, CreateFileSignedUrlSupabase Storage
GenerateImageAn image API
ExtractDataFromUploadedFileAn LLM with file input, or a document-parsing API
Automations (scheduled)pg_cron or a scheduled Edge Function
Automations (on data change)Database webhooks or triggers

If your app used a contact form that emailed you through SendEmail, a hosted form backend can replace both the email and the storage with no function at all. See the contact form guide.

Step 7: move the files

Uploaded files live on Base44’s storage, and your records hold their URLs. Download each file, upload it to Supabase Storage, and rewrite the URL in the record. Do it before you close your Base44 account.

Step 8: host it and switch over

Build the frontend and deploy it (see self-hosting), point your domain at the new host, and recreate redirects. Remember that Base44’s SEO pre-rendering stays behind: a client-rendered React app on Vercel or Netlify serves crawlers an empty shell until you add pre-rendering or move to a framework with server rendering.

What to do next

  1. Run the SDK count and write the migration list.
  2. Create the Supabase tables with Base44’s IDs as primary keys.
  3. Migrate one entity end to end, from table to UI, before touching the rest.

Found something wrong or out of date? Base44 ships changes every week and we'd rather fix a guide than let it rot.

Frequently asked questions

Can Base44 use Supabase as its database?
Not as a drop-in replacement for Base44's own database. You can call Supabase from backend functions or custom integrations, but Base44's entities, auth and built-in integrations run on Base44. A full move to Supabase means replacing the Base44 SDK calls in your code.
How long does a Base44 to Supabase migration take?
It depends on how many SDK calls your app makes. An app with a few entities and a few dozen calls can move in a weekend. One with hundreds of calls, AI agents, automations and many integrations is a multi-week project.
Can I keep my users' passwords when leaving Base44?
Base44 doesn't document a way to export password hashes. Import your users' records into Supabase and send each user a password reset link or switch them to magic-link or social login.
Will my record URLs change when I move to Supabase?
They don't have to. Base44 record IDs are strings. Keep them as the primary key in Supabase, as a text column, and your existing URLs like /Product?id=... keep working.
What replaces Base44 backend functions in Supabase?
Supabase Edge Functions. Both run on Deno and TypeScript, so most function logic moves across with changes only to how the function reads data and secrets.

Read next