Skip to content

feat(connections): add pausedSyncs to connection list API#5699

Merged
agusayerza merged 5 commits into
masterfrom
agus/NAN-5076/connection-list-refresh-be
Apr 7, 2026
Merged

feat(connections): add pausedSyncs to connection list API#5699
agusayerza merged 5 commits into
masterfrom
agus/NAN-5076/connection-list-refresh-be

Conversation

@agusayerza
Copy link
Copy Markdown
Contributor

@agusayerza agusayerza commented Mar 24, 2026

Add pausedSyncs field to the connections list API response.

We want to show which connections have paused syncs on the FE when listing connections. For each page of connections returned, we fetch their syncs from the DB and do a targeted lookup to the orchestrator using exact schedule names, bounded by page size (~20 connections) rather than the full environment.

  • Add pausedSyncs: string[] to ApiConnectionSimple type
  • Add getSyncsByConnectionIds() to sync service
  • Use existing Orchestrator.searchSchedules() with exact names for the current page
  • Graceful fallback: orchestrator errors log and return empty pausedSyncs rather than 500ing the entire list endpoint

This change also formalizes the enrichment flow across shared contracts and service boundaries, including safer failure handling and tighter validation on schedule-search inputs, so paused-sync visibility is added in a resilient, bounded way without compromising the stability of the connections listing path.


This summary was automatically generated by @propel-code-bot

@agusayerza agusayerza requested a review from a team March 24, 2026 21:24
@linear
Copy link
Copy Markdown

linear Bot commented Mar 24, 2026

propel-code-bot[bot]

This comment was marked as outdated.

provider: data.provider,
activeLog: data.active_logs,
endUser: data.end_user,
hasPausedSyncs: pausedConnectionIds.has(data.connection.id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we instead return a list of paused sync names and let the client figure out the boolean just based on .length > 0? We've done all the heavy lifting for it already at this point and so it's more information for free.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

};

export async function up(knex: Knex): Promise<void> {
await knex.raw(`CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_schedules_paused_name" ON ${SCHEDULES_TABLE} (name) WHERE state = 'PAUSED';`);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you run an EXPLAIN on the search query? Given how small schedules table is, this index might not be even needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had run an explain, with the old approach it would perform better, but its hard to tell if worth it. Now that we call it by page, it makes no sense so I dropped the index. Old approach seq scanned 74K rows in ~50ms without the index, for reference. The new approach hits schedules_unique_name and finishes in 0.1ms

@@ -14,6 +14,8 @@ type PostSearch = Endpoint<{
Path: typeof path;
Body: {
names?: string[] | undefined;
namePrefix?: string | undefined;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it better to add a new explicit param or to support wildcard in the existing names one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed

@@ -23,6 +25,8 @@ type PostSearch = Endpoint<{
const bodySchema = z
.object({
names: z.array(z.string().min(1)).optional(),
namePrefix: z.string().min(1).optional(),
state: z.enum(['STARTED', 'PAUSED', 'DELETED']).optional(),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recently found out you can use satisfies ScheduleState[] to prevent accidentally having an enum value that doesn't exist.

@@ -114,6 +123,19 @@ export class Orchestrator {
return Ok(scheduleMap);
}

async getPausedSyncsByEnvironment({ environmentId }: { environmentId: number }): Promise<Result<string[]>> {
const namePrefix = `environment:${environmentId}:sync:`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's expose the prefix from the orchestrator.ts to avoid duplication and make sure we only need to change it in one place if needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer needed

@@ -114,6 +123,19 @@ export class Orchestrator {
return Ok(scheduleMap);
}

async getPausedSyncsByEnvironment({ environmentId }: { environmentId: number }): Promise<Result<string[]>> {
const namePrefix = `environment:${environmentId}:sync:`;
const result = await this.client.searchSchedulesByPrefix({ namePrefix, state: 'PAUSED', limit: 10_000 });
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this limit might quickly fall apart for Linux Foundation. That's also a huge amount of data to transfer when the only thing we are interested in is a boolean.
What about a new orchestrator endpoint like GET /stats?name_prefix=... that return a count per state for now

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the DB approach to only take into consideration the connections loaded on the current page, that way we have it scoped down and don't need to resolve every sync to display the icon

@agusayerza agusayerza force-pushed the agus/NAN-5076/connection-list-refresh-be branch from 3b958df to 26ba24a Compare March 26, 2026 17:08
propel-code-bot[bot]

This comment was marked as outdated.

@agusayerza agusayerza requested a review from marcindobry March 26, 2026 17:13
Copy link
Copy Markdown
Contributor

@propel-code-bot propel-code-bot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review found no issues with the changes made.

Status: No Issues Found | Risk: Low

Review Details

📁 5 files reviewed | 💬 0 comments

Instruction Files
└── .claude/
    ├── agents/
    │   └── nango-docs-migrator.md
    └── skills

@agusayerza agusayerza changed the title feat(connections): efficient paused syncs detection via reverse lookup feat(connections): add pausedSyncs to connection list API Mar 26, 2026
@agusayerza agusayerza requested a review from a team March 26, 2026 17:30
if (syncs.length > 0) {
const scheduleResult = await orchestrator.searchSchedules(syncs.map((s) => ({ syncId: s.id, environmentId: environment.id })));
if (scheduleResult.isErr()) {
res.status(500).send({ error: { code: 'server_error' } });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we log the error or have it attached to the span somehow? otherwise will be tough to know what caused the error

const connectionIds = connections.map((data) => data.connection.id);
const syncs = await getSyncsByConnectionIds({ connectionIds });
if (syncs.length > 0) {
const scheduleResult = await orchestrator.searchSchedules(syncs.map((s) => ({ syncId: s.id, environmentId: environment.id })));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add some safeguard to orchestrator postSearch and max the length of names to a high value that is not infinite, so we don't happen to send a huge number by accident

}
return db.knex.select('id', 'name', 'nango_connection_id').from<Sync>(TABLE).whereIn('nango_connection_id', connectionIds).andWhere({ deleted: false });
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make all new functions that can fail return a Result?

Add hasPausedSyncs field to the connections list API response.

Instead of fetching all syncs for the page's connections and querying
orchestrator for each state, we now reverse the lookup: ask orchestrator
for all PAUSED schedules in the environment first (fast path returns
immediately when nothing is paused), then resolve only those sync IDs
back to connection IDs.

- Add partial index (name) WHERE state = 'PAUSED' on schedules table
- Extend scheduler/orchestrator search to support namePrefix + state
- Add Orchestrator.getPausedSyncsByEnvironment()
- Add getSyncsByIds() to sync service
- Add hasPausedSyncs to ApiConnectionSimple type
@agusayerza agusayerza force-pushed the agus/NAN-5076/connection-list-refresh-be branch from 86880df to 77df32d Compare April 2, 2026 15:02
@agusayerza agusayerza added this pull request to the merge queue Apr 7, 2026
Merged via the queue into master with commit 16d98d4 Apr 7, 2026
25 checks passed
@agusayerza agusayerza deleted the agus/NAN-5076/connection-list-refresh-be branch April 7, 2026 21:18
praneeth-oai pushed a commit to praneeth-oai/nango that referenced this pull request Apr 23, 2026
Refresh the connections list UI with improved filtering and status
indicators


https://www.figma.com/design/2MyZwo8A4D2XRIOhXL3cZ8/CURRENT-%F0%9F%90%BA-Design-Sytem---Product?node-id=9094-77970&m=dev

[Loom demo](https://www.loom.com/share/0c3d7958981b43f59088471464dcb0ae)

- Replace MultiSelect with new Combobox component (single +
multi-select)
- Add granular Status filter: OK, Error, Auth error, Sync error, Paused
syncs
- Status filter persisted in URL query params
- Show hasPausedSyncs indicator per connection row
- Integration filter now shows logos and "Add integration" footer CTA

<!-- Describe the problem and your solution --> 

<!-- Issue ticket number and link (if applicable) -->
Requires NangoHQ#5699 to be merged

NAN-5076
<!-- Testing instructions (skip if just adding/editing providers) -->


<!-- Summary by @propel-code-bot -->

---

This update also standardizes filtering behavior by combining URL-backed
status state with frontend status composition logic layered on top of
existing backend error filtering, while introducing reusable UI building
blocks intended to support richer filter hierarchies and status
rendering patterns across the app.

---
*This summary was automatically generated by @propel-code-bot*
bahdcoder pushed a commit to usehivy/integrations that referenced this pull request Apr 28, 2026
Refresh the connections list UI with improved filtering and status
indicators


https://www.figma.com/design/2MyZwo8A4D2XRIOhXL3cZ8/CURRENT-%F0%9F%90%BA-Design-Sytem---Product?node-id=9094-77970&m=dev

[Loom demo](https://www.loom.com/share/0c3d7958981b43f59088471464dcb0ae)

- Replace MultiSelect with new Combobox component (single +
multi-select)
- Add granular Status filter: OK, Error, Auth error, Sync error, Paused
syncs
- Status filter persisted in URL query params
- Show hasPausedSyncs indicator per connection row
- Integration filter now shows logos and "Add integration" footer CTA

<!-- Describe the problem and your solution --> 

<!-- Issue ticket number and link (if applicable) -->
Requires NangoHQ#5699 to be merged

NAN-5076
<!-- Testing instructions (skip if just adding/editing providers) -->


<!-- Summary by @propel-code-bot -->

---

This update also standardizes filtering behavior by combining URL-backed
status state with frontend status composition logic layered on top of
existing backend error filtering, while introducing reusable UI building
blocks intended to support richer filter hierarchies and status
rendering patterns across the app.

---
*This summary was automatically generated by @propel-code-bot*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants