triplit icon indicating copy to clipboard operation
triplit copied to clipboard

Batched changesets can violate user-defined permissions due to unordered record submission

Open jakobrosenberg opened this issue 10 months ago • 2 comments

When the Triplit client batches multiple set operations, it doesn't preserve the order in which they were made. This causes problems when one record's permission check depends on another—e.g., a team_member referencing a team_id that hasn't been created yet.

In online mode, this may cause permission errors server-side if the server receives and applies the team_member before the referenced team exists.

In offline mode, this issue is more severe. A user may queue hours or days of work offline, only for sync to fail due to invalid dependencies in permissions, leading to lost data or partial syncs.

Example

{
  "type": "CHANGES",
  "payload": {
    "changes": {
      "json": {
        "team_members": {
          "sets": [
            [
              "team_member_id",
              {
                "team_id": "team_id", // ← doesn't exist yet
                ...
              }
            ]
          ]
        },
        "teams": {
          "sets": [
            [
              "team_id",
              {
                "id": "team_id",
                ...
              }
            ]
          ]
        }
      }
    }
  }
}

jakobrosenberg avatar May 07 '25 08:05 jakobrosenberg

Proposed Solutions

Preserve Operation Order (Flat List): Send operations as a flat, ordered array. Each operation includes the table, type (set/delete), key, and value.

Example:

[
  {
    "table": "teams",
    "op": "set",
    "key": "team_id",
    "value": { "id": "team_id", "name": "My Team", ... }
  },
  {
    "table": "team_members",
    "op": "set",
    "key": "team_member_id",
    "value": { "id": "team_member_id", "team_id": "team_id", ... }
  }
]

Add Optional Order Index to Set Entries: Allow each set to include an explicit numeric order, which the server can use to sort operations before applying them.

Example:

"team_members": {
  "sets": [
    [
      "team_member_id",
      3,
      { "id": "team_member_id", "team_id": "team_id", ... }
    ]
  ]
},
"teams": {
  "sets": [
    [
      "team_id",
      2,
      { "id": "team_id", "name": "My Team", ... }
    ]
  ]
}

Both of these approaches would allow client applications to preserve the logical intent of user actions and avoid permission-related sync failures. This is especially critical in offline scenarios, where retrying or reordering operations after failure is much harder or impossible without user intervention.

jakobrosenberg avatar May 07 '25 08:05 jakobrosenberg

This is definitely a bug where transactions from the client that should pass permissions are being rejected in some cases. We've already shipped some improvements that may fix your exact scenario but we're revamping the system to ensure that this doesn't happen ever.

matlin avatar May 16 '25 18:05 matlin