| title | Supabase Replication Plugin for RxDB - Real-Time, Offline-First Sync |
|---|---|
| slug | replication-supabase.html |
| description | Build real-time, offline-capable apps with RxDB + Supabase. Push/pull changes via PostgREST, stream updates with Realtime, and keep data in sync across devices. |
| image | /headers/replication-supabase.jpg |
import {Tabs} from '@site/src/components/tabs'; import {Steps} from '@site/src/components/steps'; import {VideoBox} from '@site/src/components/video-box'; import {RxdbMongoDiagramPlain} from '@site/src/components/mongodb-sync';
The Supabase Replication Plugin for RxDB delivers seamless, two-way synchronization between your RxDB collections and a Supabase (Postgres) table. It uses PostgREST for pull/push and Supabase Realtime (logical replication) to stream live updates, so your data stays consistent across devices with first-class local-first, offline-ready support.
Under the hood, the plugin is powered by the RxDB Sync Engine. It handles checkpointed incremental pulls, robust retry logic, and conflict detection/resolution for you. You focus on features—RxDB takes care of sync.
- Cloud Only Backend: No self-hosted server required. Client devices directly sync with the Supabase Servers.
- Two-way replication between Supabase tables and RxDB collections
- Offline-first with resumable, incremental sync
- Live updates via Supabase Realtime channels
- Conflict resolution handled by the RxDB Sync Engine
- Works in browsers and Node.js with
@supabase/supabase-js
Clients connect **directly to Supabase** using the official JS client. The plugin:
- Pulls documents over PostgREST using a checkpoint
(modified, id)and deterministic ordering. - Pushes inserts/updates using optimistic concurrency guards.
- Streams new changes using Supabase Realtime so live replication stays up to date.
:::note Because Supabase exposes Postgres over HTTP/WebSocket, you can safely replicate from browsers and mobile apps. Protect your data with Row Level Security (RLS) policies; use the anon key on clients and the service role key only on trusted servers. :::
npm install rxdb @supabase/supabase-jsIn your supabase project, create a new table. Ensure that:
- The primary key must have the type text (Primary keys must always be strings in RxDB)
- You have an modified field which stores the last modification timestamp of a row (default is
_modified) - You have a boolean field which stores if a row should is "deleted". You should not hard-delete rows in Supabase, because clients would miss the deletion if they haven't been online at the deletion time. Instead, use a deleted
booleanto mark rows as deleted. This way all clients can still pull the deletion, and RxDB will hide the complexity on the client side. - Enable the realtime observation of writes to the table.
Here is an example for a "human" table:
create extension if not exists moddatetime schema extensions;
create table "public"."humans" (
"passportId" text primary key,
"firstName" text not null,
"lastName" text not null,
"age" integer,
"_deleted" boolean DEFAULT false NOT NULL,
"_modified" timestamp with time zone DEFAULT now() NOT NULL
);
-- auto-update the _modified timestamp
CREATE TRIGGER update_modified_datetime BEFORE UPDATE ON public.humans FOR EACH ROW
EXECUTE FUNCTION extensions.moddatetime('_modified');
-- add a table to the publication so we can subscribe to changes
alter publication supabase_realtime add table "public"."humans";Create a normal RxDB database, then add a collection whose schema mirrors your Supabase table. The primary key must match (same column name and type), and fields should be top-level simple types (string/number/boolean). You don’t need to model server internals: the plugin maps the server’s _deleted flag to doc._deleted automatically, and _modified is optional in your schema (the plugin strips it on push and will include it on pull only if you define it). For browsers use a persistent storage like Localstorage or IndexedDB. For tests you can use the in-memory storage.
// client
import { createRxDatabase } from 'rxdb/plugins/core';
import { getRxStorageLocalstorage } from 'rxdb/plugins/storage-localstorage';
export const db = await createRxDatabase({
name: 'mydb',
storage: getRxStorageLocalstorage()
});
await db.addCollections({
humans: {
schema: {
version: 0,
primaryKey: 'passportId',
type: 'object',
properties: {
passportId: { type: 'string', maxLength: 100 },
firstName: { type: 'string' },
lastName: { type: 'string' },
age: { type: 'number' }
},
required: ['passportId', 'firstName', 'lastName']
}
}
});Make a single Supabase client and reuse it across your app. In the browser, use the anon key (RLS-protected). On trusted servers you may use the service role key—but never ship that to clients.
//> client
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
'https://xyzcompany.supabase.co',
'eyJhbGciOi...'
);//> client
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL!, // e.g. https://xyzcompany.supabase.co
import.meta.env.VITE_SUPABASE_ANON_KEY! // anon key for browsers
// optional options object here
);//> client
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
'http://127.0.0.1:54321',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
);Connect your RxDB collection to the Supabase table to start the replication.
//> client
import { replicateSupabase } from 'rxdb/plugins/replication-supabase';
const replication = replicateSupabase({
tableName: 'humans',
client: supabase,
collection: db.humans,
replicationIdentifier: 'humans-supabase',
live: true,
pull: {
batchSize: 50,
// optional: shape incoming docs
modifier: (doc) => {
// map nullable age-field
if (!doc.age) delete doc.age;
return doc;
}
// optional: customize the pull query before fetching
queryBuilder: ({ query }) => {
// Add filters, joins, or other PostgREST query modifiers
// This runs before checkpoint filtering and ordering
return query.eq("status", "active");
},
},
push: {
batchSize: 50
},
// optional overrides if your column names differ:
// modifiedField: '_modified',
// deletedField: '_deleted'
});
// (optional) observe errors and wait for the first sync barrier
replication.error$.subscribe(err => console.error('[replication]', err));
await replication.awaitInitialReplication();:::note Nullable values must be mapped
Supabase returns null for nullable columns, but in RxDB you often model those fields as optional (i.e., they can be undefined/missing). To avoid schema errors, map null → undefined in the pull.modifier (usually by deleting the key).
:::
You can use the pull.queryBuilder to use joins and also pull data from related tables.
To do that, you have to create a new query object in the pull.queryBuilder with the .select() method and return it.
const replication = replicateSupabase({
pull: {
queryBuilder: (/* ignore the passed query instance from here */) => {
/**
* Create a totally new query instance
* and return that.
*/
return supabase.from('humans').select('*, pets(*), toys(*)');
}
}
});The RxSupabaseReplicationState which is returned from replicateSupabase() allows you to run all functionality of the normal RxReplicationState.
Why use Supabase and RxDB as a cloud document database for Node.js and TypeScript?
Supabase and RxDB offer the best of both worlds for Node.js and TypeScript applications. Supabase provides a powerful PostgreSQL backend. You can use PostgreSQL as a document database by storing JSON data. RxDB provides a reactive local document store. You achieve real-time synchronization between the local document store and the Supabase backend. You benefit from strong TypeScript typing on the client and robust SQL querying on the server.
- Replication API Reference: Learn the core concepts and lifecycle hooks — Replication
- Offline-First Guide: Caching, retries, and conflict strategies — Local-First
- Supabase Essentials:
- Row Level Security (RLS) — https://supabase.com/docs/guides/auth/row-level-security
- Realtime — https://supabase.com/docs/guides/realtime
- Local dev with the Supabase CLI — https://supabase.com/docs/guides/cli
- Community: Questions or feedback? Join our Discord — Chat