Skip to content

Bug: Supabase replication plugin doesn't remove _modified field on INSERT and ignores push.modifier #7513

@biokys

Description

@biokys

Description

The official RxDB Supabase replication plugin has two related bugs in the push handler:

  1. push.modifier is never applied - The plugin works directly with row.newDocumentState without calling the modifier function
  2. _modified field is not removed for INSERT operations - It's only removed for UPDATE operations, causing PostgreSQL errors when the field is sent during inserts

Environment

  • RxDB Version: 16.20.0
  • Plugin: rxdb/plugins/replication-supabase
  • Database: Supabase (PostgreSQL) with moddatetime trigger for automatic _modified management
  • Platform: React Native (Expo SDK 53)

Current Behavior

When creating a new document (INSERT operation), the plugin sends the _modified field to Supabase:

{
  "id": "...",
  "amount": 1000,
  "_modified": 1761842752620,
  "created_at": "2025-10-30T10:05:52.619Z"
}

This causes issues because:

  • Supabase's moddatetime trigger should set _modified automatically
  • Sending numeric timestamp (milliseconds) causes PostgreSQL error: "date/time field value out of range"

Expected Behavior

The plugin should:

  1. Apply push.modifier if provided in configuration
  2. Remove _modified field for both INSERT and UPDATE operations (currently only UPDATE removes it)

Root Cause

Looking at the plugin source code (node_modules/rxdb/dist/esm/plugins/replication-supabase/index.js):

Bug 1: Push modifier is never called (lines ~109-168)

var replicationPrimitivesPush = options.push ? {
  async handler(rows) {
    // BUG: modifier is never applied here
    await Promise.all(rows.map(async row => {
      var newDoc = row.newDocumentState;  // Works directly with doc
      if (!row.assumedMasterState) {
        var c = await insertOrReturnConflict(newDoc);  // INSERT
      } else {
        var _c = await updateOrReturnConflict(newDoc, row.assumedMasterState);  // UPDATE
      }
    }));
  }
} : undefined;

Fix needed: Apply modifier before calling insert/update functions:

var newDoc = row.newDocumentState;
if (options.push.modifier) {
  newDoc = await options.push.modifier(newDoc);
}

Bug 2: INSERT doesn't remove _modified (lines ~111-125)

async function insertOrReturnConflict(doc) {
  var id = doc[primaryPath];
  var { error } = await options.client
    .from(options.tableName)
    .insert(doc);  // BUG: doc contains _modified field

  if (error) {
    if (error.code === '23505') {
      // Duplicate key - fetch current state
      return await fetchById(id);
    }
    throw error;
  }
  return undefined;
}

Compare with UPDATE which correctly removes it (lines ~126-154):

async function updateOrReturnConflict(doc, assumedMasterState) {
  var toRow = flatClone(doc);

  // CORRECT: modified field will be set server-side
  delete toRow[modifiedField];

  var query = options.client.from(options.tableName).update(toRow);
  // ... rest of update logic
}

Fix needed: Remove _modified before INSERT:

async function insertOrReturnConflict(doc) {
  var toRow = flatClone(doc);
  delete toRow[modifiedField];  // Add this line

  var { error } = await options.client
    .from(options.tableName)
    .insert(toRow);  // Use cleaned doc
  // ... rest
}

Reproduction Steps

  1. Setup Supabase with moddatetime trigger on a table:
CREATE TRIGGER handle_updated_at BEFORE UPDATE ON expenses
FOR EACH ROW EXECUTE FUNCTION moddatetime (_modified);
  1. Configure replication with push modifier:
import { replicateSupabase } from 'rxdb/plugins/replication-supabase';

const replication = replicateSupabase({
  collection: myCollection,
  client: supabaseClient,
  tableName: 'expenses',
  replicationIdentifier: 'expenses-sync',
  push: {
    batchSize: 10,
    modifier: (doc) => {
      const clean = { ...doc };
      delete clean._modified;  // Try to remove it
      delete clean._rev;
      delete clean._meta;
      return clean;
    }
  }
});
  1. Insert a new document:
await myCollection.insert({
  id: '123',
  amount: 1000,
  created_at: new Date().toISOString()
});
  1. Check network request - you'll see _modified is still present despite the modifier

Workaround

Currently using a locally patched version of the plugin with both fixes applied.

Suggested Fix

Apply both fixes to the official plugin:

  1. In push handler: Apply modifier before calling insert/update
  2. In insertOrReturnConflict: Remove _modified field (same as UPDATE does)

This would align the INSERT behavior with UPDATE and properly respect the push.modifier configuration.

Impact

This affects anyone using:

  • RxDB Supabase replication with server-side timestamp triggers
  • PostgreSQL moddatetime extension
  • Custom push.modifier functions that need to transform documents before sync

The bug causes sync failures and makes it impossible to properly handle timestamp fields that should be set server-side.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions