Skip to content

Comparison

All five solve the same problem - read and write locally, converge later - and differ in what you have to run, what the client stores, and who decides what a client may see. One note before the table: Supapower builds on PGlite, which is an Electric SQL project, but does not use Electric’s sync service - the two are unrelated beyond sharing that dependency.

Supapower RxDB + Supabase plugin PowerSync Electric Zero
Extra service to run None - Supabase’s Data API and Realtime None - the same Data API and Realtime PowerSync Service (cloud or self-hosted) Electric sync service (cloud or self-hosted) zero-cache server (cloud or self-hosted)
Access to your database None beyond the Supabase client (HTTPS + websocket) None beyond the Supabase client (HTTPS + websocket) Logical replication connection to your database Logical replication connection to your Postgres Logical replication connection to your Postgres
Local store PGlite - Postgres compiled to WASM Pluggable; localStorage and Dexie free, IndexedDB/OPFS/SQLite paid SQLite Your choice; PGlite, TanStack DB or plain JS state Its own client store
Local queries SQL (PostgreSQL) Mango-style selectors over JSON documents SQL (SQLite) Whatever your client store offers ZQL
Backends supported Supabase only Supabase, CouchDB, GraphQL, Firestore or any HTTP backend Postgres, MongoDB, MySQL, SQL Server and more Any Postgres with logical replication Any Postgres with logical replication
Direction Two-way Two-way Two-way Read path only - you build the write path Two-way
What a client may sync Your RLS policies, plus an optional per-table filter Your RLS policies, plus a client-side pull query Sync Streams, configured separately from RLS Shapes, scoped by your auth proxy ZQL queries plus permission rules in app code
Write path Queued locally, upserted through PostgREST Queued locally, pushed through PostgREST with concurrency guards Your own upload endpoint (uploadData) Your own API Custom mutators on the server
Conflict model Per-column merge in Postgres, last-write-wins within a column Per document; the server version wins unless you write a handler Server-authoritative, last-write-wins per field Yours to define Server-authoritative; optimistic client state is rolled back
Partial sync of a big table Yes - a per-table filter callback, resolved per session Yes - pull query filters Yes - Sync Streams Yes - Shapes Yes - queries
Demands on your schema A single-column primary key; updated_at to sync incrementally String primary keys, plus _deleted and _modified columns Client-side schema in app code None None
Client platforms Web, Node, Bun, Deno (JS/TS) Web, Node, React Native, Electron, Capacitor Web, React Native, Flutter, Kotlin, Swift Web and anything that can speak its HTTP API Web
License Apache-2.0 Apache-2.0 core; paid license for the faster storages SDKs Apache-2.0; service under FSL 1.1 with an Apache-2.0 future license Apache-2.0 Apache-2.0
Cost model Only what Supabase already costs you Supabase, plus a per-year license if you need a premium storage Free tier, then metered usage Free self-hosted; metered cloud Free self-hosted; per-instance cloud
Maturity Pre-1.0, no commercial support Production, paid support available Production, commercial support Production, commercial support 1.0, commercial support

RxDB with its Supabase replication plugin is the closest architectural match, and the reason the first two rows of the table look identical: it also talks straight to Supabase over PostgREST and Realtime, so “nothing to deploy” is not unique to Supapower. What differs is the local database and the conflict model. RxDB is a document store queried with Mango-style selectors - no SQL, no joins, no Postgres - and conflicts are resolved per document, where the default handler drops the local version in favor of the server’s and merging two edits means writing a handler yourself. It also asks your Supabase tables for string primary keys and _deleted / _modified columns. Choose it for a document model, React Native, or one client syncing several kinds of backend. Choose Supapower to keep writing SQL against a real Postgres and to get column-level merging without writing the merge.

PowerSync is the closest relative in intent and Supapower’s stated inspiration - it’s also Supabase’s own listed offline-first integration. Choose it for non-Supabase or non-Postgres backends, native mobile SDKs, Sync Streams that carve a large table into per-user buckets, or paid support and uptime SLAs. Supapower’s trade: no service to run, no replication slot on your database, and RLS instead of a second rules language to maintain.

Electric is a read-path sync engine: Shapes delivered over HTTP and cached by a CDN, deliberately leaving writes to the application to design. Choose it for large read-heavy fan-out, or a Postgres that is not Supabase. Supapower ships the write path instead - the queue, the retries, the per-column conflict rules - so there’s nothing left to build.

Zero syncs whatever a query asks for and applies writes through server-authoritative mutators - closest to “your backend, but instant”. Choose it when authorization and mutation logic belong in server code and the working set is a query, not a table. Supapower’s authorization is the database’s own: no second permission model to write.

In every case it does come down to a filter attached to a table - what differs is who writes the filter, and whether a client can change it at runtime.

  • Supapower - a filter callback on the table config, composed in client code, is handed the current session so it can key off a claim (e.g. filter.eq('workspace_id', session.user.app_metadata.workspace_id)), and is applied to both the PostgREST download and the postgres_changes subscription. Conditions are ANDed, with no OR, no subqueries and no joins, so a membership rule only fits as an in list of at most 100 values. RLS is still what stops a client widening its own filter. See filter.
  • RxDB - the Supabase plugin takes a pull.queryBuilder, so the pull is an ordinary PostgREST query you can narrow (query.eq('status', 'active')) or point at joined tables. The filter lives in client code, so RLS is still what stops a client from asking for more. See Supabase replication.
  • PowerSync - a stream is a SQL-like query in a YAML config deployed to the service, e.g. SELECT * FROM lists WHERE owner_id = auth.user_id(). Filters take parameters from the token, the connection or the client’s own subscription, and each distinct parameter value becomes a bucket that syncs as a unit. See Sync Streams.
  • Electric - a shape is one table plus an optional where clause and columns list, requested over HTTP; subqueries cover membership and sharing rules (id IN (SELECT user_id FROM memberships WHERE org_id = $1)). Shape definitions are immutable, and production apps are expected to have their backend issue them so a client cannot widen its own filter. See Shapes.
  • Zero - filters are ordinary queries in application code. zero-cache asks your server to resolve each query by name, which is where it can be narrowed further for permissions, and a query can pull in relationships, so the unit is a query result rather than a single table. See Queries.

The filter is what narrows both the download and the subscription; cursor still only narrows how far back in time a download reads, not which rows qualify. RLS still decides what the filter is allowed to return - a filter callback can only narrow further, never widen past it. A filter resolved from the session is re-resolved on every auth change, and a change to it empties the local table and downloads it again.

When a table is bigger than the narrowest slice a column filter can express - a membership rule that needs a join, say - splitting the hot subset into its own table - optionally in a separate schema - is the answer for that slice.

  • WatermelonDB - a SQLite-backed reactive database for React and React Native; you implement the sync backend yourself against its pull/push protocol.
  • LiveStore - an event-sourced SQLite store with pluggable sync backends (Cloudflare Durable Objects first-party).
  • Triplit - a full-stack syncing database with CRDT-based conflict resolution, requiring its own server (@triplit/server).
  • Verdant - an IndexedDB store plus a small Node and SQLite sync server of its own, MIT-licensed and openly experimental; it replaces the backend rather than syncing an existing Supabase one, which is why it is not in the table above.

Checked against the linked docs in September 2026. Sync engines move fast - follow the link before betting on a row.