Coming from Supabase

Plexivo Postgres speaks the same Data API and policy language as Supabase. Map supabase-js and supabase_flutter calls, keep your policies, and move.

If you know Supabase, you know Plexivo Postgres. The Data API reads PostgREST query strings, the policy helpers have the same names, so auth.uid() in your policies keeps working, and the client libraries take Supabase’s method names. Your database and its users move with their ids and passwords, and your app runs in Mumbai, with backups copied to Hyderabad.

The client, mapped#

supabase-js@plexivo/client
createClient(url, anonKey)createClient(projectUrl, publishableKey)
from(t).select('a, b:c(d)', { count: 'exact' })The same
eq, neq, gt, in, is, ilike, contains, or, not, match, textSearch, filterThe same
order(c, { ascending, nullsFirst, referencedTable }), range, limit, single, maybeSingleThe same
insert(v).select(), update(v).eq(…), delete().eq(…), upsert(v, { onConflict, ignoreDuplicates })The same
rpc(fn, args) with filters after itThe same
const { data, error } = await …The same with createClient(…, { throwOnError: false }); by default errors are thrown
auth.signUp({ email, password, options: { data } })The same
auth.signInWithPassword, signInWithOtp, verifyOtp, signInAnonymously, signInWithOAuth, exchangeCodeForSession, updateUser({ data }), onAuthStateChange, signOutThe same
auth.getSession(), auth.getUser()The same, returning the session and the user themselves rather than inside data
channel(n).on('postgres_changes', { table, filter }, cb)channel('changes:table', { filter }).onChange(cb)
channel(n).on('broadcast', { event }, cb), track()channel('room:n', { presence }).onBroadcast(event, cb), onPresence(cb)
functions.invoke(name, { body })The same
supabase_flutterplexivo_client
Supabase.instance.clientPlexivoClient(projectUrl, publishableKey)
from(t).select().eq(…).order(c, ascending: false)from(t).select().eq(…).order(c, 'desc')
inFilter, isFilter, range, single, maybeSingleThe same
insert, update, upsert, delete, rpcThe same, plus create that returns the new row
auth.signUp(email:, password:, data:), auth.signInWithPasswordsignUp(email:, password:, data:), signInWithPassword
auth.onAuthStateChangeauthStateChanges
from(t).stream(primaryKey: ['id'])channel('changes:t').changes

The same code#

sql
create table profiles (id uuid primary key default auth.uid(), username text unique not null);
create table posts (
  id bigint generated always as identity primary key,
  author_id uuid not null default auth.uid() references profiles (id),
  title text not null,
  published boolean not null default false
);
alter table profiles enable row level security;
alter table posts enable row level security;

-- Policies from Supabase move as they are
create policy "Public profiles" on profiles for select using (true);
create policy "Own profile" on profiles for insert with check (auth.uid() = id);
create policy "Read published or own" on posts for select using (published or auth.uid() = author_id);
create policy "Write own" on posts for all using (auth.uid() = author_id) with check (auth.uid() = author_id);

create function published_count() returns bigint language sql stable security invoker
  as $$ select count(*) from posts where published $$;
comment on function published_count() is '@expose';
js
import { createClient } from '@plexivo/client';

const supabase = createClient(PLEXIVO_DATA_URL, PLEXIVO_PUBLISHABLE_KEY, { throwOnError: false });

const { error: signUpError } = await supabase.auth.signUp({ email, password, options: { data: { username: 'asha' } } });
await supabase.from('profiles').insert({ username: 'asha' });

const { data: post, error } = await supabase.from('posts').insert({ title: 'Hello', published: true }).select().single();
await supabase.from('posts').update({ title: 'Hello, world' }).eq('id', post.id);

const { data: posts, count } = await supabase.from('posts')
  .select('id, title, author:profiles(username)', { count: 'exact' })
  .eq('published', true)
  .order('id', { ascending: false })
  .range(0, 9);

const { data: published } = await supabase.rpc('published_count');
console.log(posts[0].author.username, count, published);
dart
import 'package:plexivo_client/plexivo_client.dart';

final supabase = PlexivoClient(dataUrl, publishableKey);
await supabase.signUp(email: email, password: password, data: {'username': 'ravi'});
await supabase.from('profiles').insert({'username': 'ravi'});

final posts = await supabase.from('posts')
    .select('id, title, author:profiles(username)')
    .eq('published', true)
    .order('id', 'desc')
    .range(0, 9);

What you can do in addition#

  • Write queries in GORM’s style when you prefer: where, preload, joins, whereHas, paginate, firstOrCreate. See Joins.
  • Order by a related row’s column, and filter by a subquery, from the browser. See Queries.
  • Commit several operations from your app in one transaction, with row locks.
  • Count, sum and group rows with aggregate and groupBy.

Move your project#

  1. Create the backend. Follow Build a backend in five minutes for a database with the Data API on.
  2. Bring the database. Move your Supabase database with Migrate to Plexivo Postgres. Your tables, data, functions and policies come with it.
  3. Bring your users. From your server, with the secret key, call auth.admin.importFromSupabase(). Every user keeps their id, so rows and policies keep matching, and their password, confirmed email, metadata and Google, GitHub and Microsoft sign-ins keep working.
  4. Switch the app. Change the import to @plexivo/client (or plexivo_client), and pass your Project URL and publishable key. Rename realtime channels to changes:<table>.

Next steps#

Updated 28 October 2026