Back to Tutorials

Migrate from Supabase to Convex in an Expo App

Move an Expo React Native app to a typed, reactive Convex backend without treating a database cutover as a single deploy. This guide covers the architecture, data model, identity, data transfer, validation, and release plan.

Updated August 14, 2026 15 min read Paddy B
ExpoReact NativeSupabaseConvexMigration

Start with a parallel migration

The safe approach: keep the existing backend as the system of record while Convex passes data, authorization, and release validation. Migrate one vertical slice at a time, then use a staged release and a defined rollback window for the final cutover.

A Supabase-to-Convex move changes more than a database client. It can replace client-directed SQL and PostgREST calls, realtime channels, edge functions, and parts of the authentication setup. Treat each of those as a separate design decision.

Supabase remains a strong fit when PostgreSQL, SQL interoperability, arbitrary joins, reporting tools, external database access, and mature row-level security are central to the product. Convex is especially compelling when a TypeScript-first application benefits from a small typed API, server-side authorization, and automatically reactive data.

Define the target boundary

The mobile app should call public Convex functions only. It should never receive a deployment key, service-role key, or any other privileged credential. The public deployment URL is safe to expose; server-side secrets belong in Convex environment variables.

Expo client | typed queries / mutations / actions v Convex React client + authenticated provider | WebSocket / RPC v Convex functions |-- authentication and authorization checks |-- document database and indexes |-- scheduled jobs, storage, search, integrations

That boundary centralizes authorization and makes the client/server contract visible in generated TypeScript types. It also means you must consciously reimplement responsibilities that used to live in database policies or edge functions.

Connect Convex to Expo

Link the Expo project to EAS and agree on a Convex region before creating a deployment. A deployment cannot be moved between regions in place, so choose deliberately.

eas integrations:convex:connect \\ --region aws-eu-west-1 \\ --team-name "<team-name>" \\ --project-name "<convex-project-name>" npx convex dev

The EAS connection can install the package, create a local configuration, and configure the public deployment URL for development, preview, and production. Running npx convex dev creates the convex/ directory, generates typed API files, and deploys development function changes while it runs.

Keep deployment keys private

Commit the source-controlled Convex files, but do not commit .env.local or deployment keys. Fail early in development and CI when EXPO_PUBLIC_CONVEX_URL is absent, and verify every EAS environment points to the intended deployment.

Design documents around application paths

Do not mechanically copy the SQL schema. Start with the app’s important reads and writes, then choose document shapes, ownership fields, and indexes to serve those paths.

Existing concernConvex decision
Primary keyKeep a legacyId only where imports, links, or reconciliation need it; otherwise use Convex IDs.
Foreign keyStore a related document or stable external ID, then validate ownership in functions.
RLS policyTranslate it into reusable authentication and authorization helpers called by every public function.
Filter or orderingAdd and review the matching index before moving the UI path.
Edge function or triggerRebuild it as a mutation, action, scheduled task, or explicit integration.

Schema and indexes are backend API design. Review them alongside UI queries, particularly where a relational data model previously relied on joins or database-enforced constraints.

Move one vertical slice at a time

For each feature, migrate the schema, authorization helper, server functions, UI hook, tests, and rollout path together. A basic query and mutation can look like this:

// convex/items.ts import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; export const listForCurrentUser = query({ args: {}, handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); return ctx.db.query("items") .withIndex("by_owner", (q) => q.eq("ownerSubject", identity.subject)) .collect(); }, }); export const create = mutation({ args: { title: v.string() }, handler: async (ctx, { title }) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated"); return ctx.db.insert("items", { ownerSubject: identity.subject, title }); }, });
const items = useQuery(api.items.listForCurrentUser); const createItem = useMutation(api.items.create);

In the UI, undefined from useQuery is loading, not an empty result. Define loading, empty, reconnecting, and mutation-pending states explicitly even though reactive subscriptions remove much of the old fetch-and-invalidate plumbing.

Make the identity decision first

Choose authentication before protected data moves. You can retain an OpenID Connect-compatible identity provider and pass its token to Convex, or adopt Convex Auth after accepting its current maturity trade-offs.

  • Authorize requests from ctx.auth.getUserIdentity(), not values supplied by the client.
  • Centralize helpers such as requireUser and ownership checks; call them in every public function.
  • During coexistence, map the previous user identifier to the Convex identity subject so ownership remains stable.
  • Do not copy password hashes into a new identity system. Use verified-identity migration where supported, a password reset campaign, or a fresh sign-in flow.

RLS does not migrate itself

Moving away from database row-level security shifts enforcement into application code. Missing a check in one public function can expose data, so automate authorization coverage and keep the helpers small enough to reuse everywhere.

Import data, then reconcile it

  1. Inventory tables, relations, enums, RLS policies, RPCs, edge functions, scheduled work, storage, authentication dependencies, and webhooks.
  2. Freeze the target schema and prepare idempotent import code or normalized CSV/JSONL files.
  3. Export a point-in-time source snapshot, including storage metadata and files where needed. Record row counts and checksums per entity.
  4. Import to a staging deployment and retain legacy ID mappings through reconciliation and the rollback window.
  5. Verify counts, uniqueness, ownership, timestamps, relations, files, and representative queries automatically where possible.

For low-write systems, a short write freeze followed by a final delta export is often the simplest cutover. For high-write systems, use a temporary server-side adapter for dual writes or a carefully monitored delta process. Never let two independent mobile clients write to both databases without a single authoritative ordering rule.

Release behind a feature flag

A feature flag or staged release gives you a way back without needing an emergency app-store release. Work through a release sequence that makes rollback real:

  1. Deploy schema and functions to development, then preview or staging.
  2. Test integration, authorization, offline and reconnect behavior, realtime updates, and destructive operations.
  3. Run the production-data import and reconciliation in staging with the same tooling.
  4. Ship an internal build and monitor function errors, client errors, latency, and data mismatches.
  5. Run the final production sync and enable the Convex path for a small cohort.
  6. Expand only after reconciliation and monitoring are clean.
  7. End fallback paths only after the rollback window closes and a tested backup exists.

Keep the previous system read-only during the agreed rollback window, retain an export, and document the restoration procedure before deleting anything. Storage, Auth, edge functions, scheduled work, webhooks, and analytics each need an explicit replacement or retention decision.

Migration completion checklist

  • Every active app path uses typed Convex functions rather than direct database access.
  • Authorization checks have automated coverage.
  • Production documents and files reconcile to the approved tolerance.
  • Development, preview, and production deployments use the correct public URLs.
  • Monitoring, alerts, and a tested backup or restore plan are in place.
  • The rollback window has closed before old infrastructure and credentials are removed.
  • Old dependencies, environment variables, edge functions, and keys are cleaned up in a separate reviewable change.

Related guides

Support tutorials