Machine-translated draft — terminology + flow still being reviewed.

Supabase → Mortar migration guide

Status: cookbook (documentation). No automated import toolpg_dump is already a good tool, and migration is mostly a config + DNS-switch problem, not a data problem.

Target audience: China-market React Native apps currently running on Supabase, forced to migrate because of stability issues accessing Supabase from mainland China + overseas billing settlement.

One-shot evaluation

# The Mortar CLI ships with an evaluator (planned; run manually for now):
mortar migrate evaluate --supabase-url=$SUPABASE_URL --supabase-key=$SUPABASE_KEY
# Output: which Supabase features your project uses + Mortar equivalents + risk points

Feature mapping

SupabaseMortar equivalentDifficulty
Database (PostgREST API)mortar.from(...) (@mortar/client)Easy — 1:1
Auth (email / OAuth)mortar.authEasy — email is fine; OAuth, you wire each provider yourself
Storage (S3-compatible)mortar.storageEasy — same API shape, buckets/keys migrate directly
Realtime (postgres_changes / broadcast / presence)mortar.realtime (subscribe / channel().on('broadcast') / channel().track())Easy — full triad; see channel pitfall below
Edge Functionsmortar.computeMedium — rewrite Deno → Node/FC
Vector (pgvector)Same (pgvector)Easy — same extension
CronPlannedFor now, use Mortar compute + a third-party cron scheduler
GraphQL❌ Not supportedMortar is REST + mortar.from(table) row API; Supabase projects using pg_graphql need to migrate to REST
Triggers / WebhooksPostgres triggers + mortar.computeMedium
BranchingPlannedManual pg_dump for now
Stripe billingWeChat Pay / AlipayNot the same — mainland customers use Chinese payment rails

Data migration: three steps

1. Copy the database

# Pull from Supabase
pg_dump $SUPABASE_DB_URL \
  --no-owner --no-acl --clean --if-exists \
  -f supabase.sql

# Push to Mortar
psql $MORTAR_DB_URL -f supabase.sql

Notes:

  • Supabase stores users in the auth.users schema; Mortar uses the app_users table. Write an INSERT SELECT to map fields across. Both use bcrypt, so passwords carry over without forcing users to reset.
  • Supabase’s storage.objects table holds metadata; the actual files live in an S3-compatible bucket. Use aws s3 sync or rclone to pull objects locally, then push them to OSS.
  • RLS: the two isolation models differ. Supabase gives each project its own database — tenants are isolated by physically separate databases, with per-user RLS (auth.uid()) inside each one. Mortar uses a shared database + a tenant_id column + RLS: isolation is a platform-applied tenant_id-level policy (tenant_id = auth.tenant_id(), where auth.tenant_id() is Mortar’s analog of Supabase’s auth.uid(), and its value is the project’s UUID) — you don’t author per-table policies. So Supabase’s per-user auth.uid() = user_id policies don’t copy verbatim: Mortar V0 stores data in the generic app_rows JSONB model and the RLS grain is the project’s tenant_id, not the end-user. Move that row-level authorization intent into the app layer (end-user JWT + dedicated endpoints); tenant_id isolation is the platform backstop.

2. Copy storage objects

# Supabase uses S3-compatible (internally R2); use rclone
rclone copy \
  :s3,provider=Other,access_key_id=X,secret_access_key=Y,endpoint=https://supa.r2.cloudflarestorage.com/:bucket \
  :s3,provider=Aliyun,access_key_id=X,secret_access_key=Y,endpoint=https://oss-cn-hangzhou.aliyuncs.com:keel-bundles/

3. DNS cutover + client SDK swap

// Before
import { createClient } from '@supabase/supabase-js';
const supa = createClient(SUPABASE_URL, SUPABASE_KEY);
const { data } = await supa.from('users').select('*');

// After
import { createClient } from '@mortar/client';
// The tenant lives in the host subdomain (Supabase's <ref>.supabase.co
// model) — there is no separate `tenant` option.
const m = createClient({ url: 'https://<tenant>.api.mortar.appunvs.com', apiKey: '...' });
const { data } = await m.from('users').select('*');

@mortar/client’s surface is deliberately close to supabase-js — the chainable API (.from(table).select() / .insert() / .update() / .delete() / .eq() / .gte()) is the same.

Common pitfalls

1. Realtime channel naming + split send/receive

  • Row changes: Supabase uses postgres_changes:public:tablename; Mortar uses mortar.realtime.subscribe({table}) (subscribe to one table’s row changes).
  • broadcast / presence: Supabase is channel.on('broadcast'…) + channel.track(), all bidirectional over a single WebSocket. Mortar’s mortar.realtime.channel(name) has the same API shape, but underneath it’s SSE-receive + HTTP-POST-send (Mortar uses no WebSocket) — transparent to the caller, behaviorally equivalent. When migrating, channel.send({type:'broadcast',…}) / track() / presenceState() port over essentially verbatim.

2. Auth provider list

Supabase ships 20+ built-in OAuth providers; Mortar currently ships only email + WeChat + Apple Sign-In. For Google / Facebook / GitHub:

  • Run a PKCE flow client-side with @keel-ai/auth-session
  • POST the resulting OAuth code to Mortar auth/oauth/callback and store it yourself

Built-in OAuth orchestration is planned.

3. Edge Function runtime

Supabase Edge Functions = Deno on Cloudflare Workers. Mortar compute = Aliyun FC (Node 20) / Tencent SCF (Node 16/18).

You’ll need to:

  • Replace Deno-specific imports (e.g. https://deno.land/x/*) with npm
  • Change Deno.env.get()process.env.X
  • Top-level await in TypeScript — Node 20 supports it; no change needed

4. Billing model differences

Supabase charges a fixed monthly fee (from $25/mo) + over-quota; Mortar charges by usage (from ¥99/mo) + tier upgrades. See pricing.md for a detailed comparison.

Real migration timeline

Below is a real customer migration (an alpha customer during the early dogfood phase):

StepTimeNotes
Schema dump + restore30 min12 tables, 5 GB
File object sync4 h80 GB OSS upload
SDK swapHalf day3000 lines of supabase-js calls → @mortar/client; mostly channel names
Realtime cutover30 minDNS TTL + ~5 min wait
Gradual rollout (10% → 50% → 100%)One weekNo incidents in between
Total8 working daysOne engineer

Reverse migration

Mortar → Supabase is also supported (decoupling responsibility). Data via pg_dump; metadata via Mortar mortar export (planned). No vendor lock-in.

Pitfalls that go away later

  • Cron → equivalent to Supabase
  • Database branches → git-style, same as Supabase