diff --git a/packages/nodes-base/nodes/Supabase/RowDescription.ts b/packages/nodes-base/nodes/Supabase/RowDescription.ts
index 3d894bcef13..c631b622f63 100644
--- a/packages/nodes-base/nodes/Supabase/RowDescription.ts
+++ b/packages/nodes-base/nodes/Supabase/RowDescription.ts
@@ -268,5 +268,25 @@ export const rowFields: INodeProperties[] = [
default: 50,
description: 'Max number of results to return',
},
+ // Supabase REST API uses PostgREST under the hood. Without a stable ORDER BY, offset-based
+ // pagination returns non-deterministic pages — the database may return the same row in both
+ // page 1 and page 2, or skip rows entirely. Adding ?order=column ensures consistent page
+ // boundaries and prevents duplicates when using Return All or Limit >= 1000.
+ // See https://supabase.com/docs/guides/api/sql-to-rest for the order parameter syntax.
+ {
+ displayName: 'Order By',
+ name: 'orderBy',
+ type: 'string',
+ displayOptions: {
+ show: {
+ resource: ['row'],
+ operation: ['getAll'],
+ },
+ },
+ default: '',
+ placeholder: 'e.g. ID or created_at.desc',
+ description:
+ 'Column(s) to order results by, e.g. ID or created_at.desc. Recommended when using Return All or Limit ≥ 1000 to avoid duplicate or missing records.',
+ },
...getFilters(['row'], ['getAll'], {}),
];
diff --git a/packages/nodes-base/nodes/Supabase/Supabase.node.ts b/packages/nodes-base/nodes/Supabase/Supabase.node.ts
index 5dd710cd17d..9b9081cfad2 100644
--- a/packages/nodes-base/nodes/Supabase/Supabase.node.ts
+++ b/packages/nodes-base/nodes/Supabase/Supabase.node.ts
@@ -382,15 +382,23 @@ export class Supabase implements INodeType {
endpoint = `${endpoint}?${encodeURI(filterString)}`;
}
- if (!returnAll) {
- qs.limit = this.getNodeParameter('limit', 0);
- }
+ const requestedLimit = !returnAll
+ ? (this.getNodeParameter('limit', 0) as number)
+ : undefined;
+
+ const orderBy = this.getNodeParameter('orderBy', i, '') as string;
let rows: IDataObject[] = [];
try {
let responseLength = 0;
do {
+ if (requestedLimit !== undefined) {
+ qs.limit = Math.min(requestedLimit - rows.length, 1000);
+ }
+ if (orderBy) {
+ qs.order = orderBy;
+ }
const newRows = await supabaseApiRequest.call(
this,
'GET',
@@ -403,7 +411,10 @@ export class Supabase implements INodeType {
responseLength = newRows.length;
rows = rows.concat(newRows);
qs.offset = rows.length;
- } while (responseLength >= 1000);
+ } while (
+ responseLength >= 1000 &&
+ (requestedLimit === undefined || rows.length < requestedLimit)
+ );
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(rows),
{ itemData: { item: i } },
diff --git a/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts b/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts
index 0b65e7ed288..efcbd43241c 100644
--- a/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts
+++ b/packages/nodes-base/nodes/Supabase/tests/Supabase.node.test.ts
@@ -62,6 +62,109 @@ describe('Test Supabase Node', () => {
return fakeExecuteFunction;
};
+ describe('getAll pagination', () => {
+ it('should make exactly one request when limit is less than 1000', async () => {
+ const supabaseApiRequest = jest
+ .spyOn(utils, 'supabaseApiRequest')
+ .mockResolvedValueOnce(Array.from({ length: 50 }, (_, i) => ({ id: i })));
+
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 50,
+ tableId: 'my_table',
+ filterType: 'none',
+ orderBy: '',
+ });
+
+ await node.execute.call(fakeExecuteFunction);
+
+ expect(supabaseApiRequest).toHaveBeenCalledTimes(1);
+
+ supabaseApiRequest.mockRestore();
+ });
+
+ it('should make exactly one request when limit equals 1000', async () => {
+ const supabaseApiRequest = jest
+ .spyOn(utils, 'supabaseApiRequest')
+ .mockResolvedValueOnce(Array.from({ length: 1000 }, (_, i) => ({ id: i })));
+
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 1000,
+ tableId: 'my_table',
+ filterType: 'none',
+ orderBy: '',
+ });
+
+ await node.execute.call(fakeExecuteFunction);
+
+ expect(supabaseApiRequest).toHaveBeenCalledTimes(1);
+
+ supabaseApiRequest.mockRestore();
+ });
+
+ it('should paginate and request only remaining rows on the last page when limit > 1000', async () => {
+ const capturedQs: IDataObject[] = [];
+ const supabaseApiRequest = jest
+ .spyOn(utils, 'supabaseApiRequest')
+ .mockImplementation(async (_method, _endpoint, _body, qs) => {
+ capturedQs.push({ ...qs });
+ return capturedQs.length === 1
+ ? Array.from({ length: 1000 }, (_, i) => ({ id: i }))
+ : Array.from({ length: 500 }, (_, i) => ({ id: i + 1000 }));
+ });
+
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'getAll',
+ returnAll: false,
+ limit: 1500,
+ tableId: 'my_table',
+ filterType: 'none',
+ orderBy: '',
+ });
+
+ await node.execute.call(fakeExecuteFunction);
+
+ expect(supabaseApiRequest).toHaveBeenCalledTimes(2);
+ expect(capturedQs[0]).toMatchObject({ limit: 1000 });
+ expect(capturedQs[0]).not.toHaveProperty('offset');
+ expect(capturedQs[1]).toMatchObject({ limit: 500, offset: 1000 });
+
+ supabaseApiRequest.mockRestore();
+ });
+
+ it('should include order parameter in the request when orderBy is set', async () => {
+ const supabaseApiRequest = jest.spyOn(utils, 'supabaseApiRequest').mockResolvedValueOnce([]);
+
+ const fakeExecuteFunction = createMockExecuteFunction({
+ resource: 'row',
+ operation: 'getAll',
+ returnAll: true,
+ tableId: 'my_table',
+ filterType: 'none',
+ orderBy: 'id',
+ });
+
+ await node.execute.call(fakeExecuteFunction);
+
+ expect(supabaseApiRequest).toHaveBeenCalledWith(
+ 'GET',
+ '/my_table',
+ {},
+ expect.objectContaining({ order: 'id' }),
+ undefined,
+ {},
+ );
+
+ supabaseApiRequest.mockRestore();
+ });
+ });
+
it('should allow filtering on the same field multiple times', async () => {
const supabaseApiRequest = jest
.spyOn(utils, 'supabaseApiRequest')