Skip to content

Connector Resource Sync

Overview

The connector resource sync system enables Floh to maintain a local cache of upstream connector data — users, groups, roles, and other resources — so that workflow steps and UI screens can query connector data without making real-time API calls on every request.

This is particularly important for connectors that manage large directories (e.g. hundreds of thousands of users, hundreds of groups) where live fetches on every action would be prohibitively slow or rate-limited.

Architecture

The sync system uses a hybrid tiered approach:

Tier Storage Use Case Examples
Tier 1 — DB Sync PostgreSQL Small-to-medium reference data that changes infrequently Groups, roles, documents
Tier 2 — Redis Cache Redis (TTL) Individual high-volume lookups during workflow execution Single-user lookups
Tier 3 — Opt-in Bulk Sync PostgreSQL Large directories, enabled explicitly with filter rules Full user directory
┌─────────────────────────────────────────────────────┐
│                   Sync Scheduler                     │
│              (BullMQ repeatable jobs)                │
│                                                      │
│   ┌──────────────┐   ┌──────────────┐               │
│   │  full sync    │   │  incremental │               │
│   │  (cron)       │   │  (cron)      │               │
│   └──────┬───────┘   └──────┬───────┘               │
│          │                  │                        │
│          ▼                  ▼                        │
│   ┌──────────────────────────────────┐              │
│   │      ConnectorSyncService        │              │
│   │                                  │              │
│   │  • Pages through connector       │              │
│   │  • Computes SHA-256 hash         │              │
│   │  • Upserts with hash comparison  │              │
│   │  • Marks unseen records stale    │              │
│   │  • Purges beyond retention       │              │
│   └────────────┬─────────────────────┘              │
│                │                                     │
│   ┌────────────▼─────────────────────┐              │
│   │   connector_resource table (PG)  │              │
│   │   connector_sync_config table    │              │
│   └──────────────────────────────────┘              │
│                                                      │
│   ┌──────────────────────────────────┐              │
│   │   ConnectorCacheService (Redis)  │◄── Tier 2    │
│   │   TTL-based per-resource cache   │              │
│   └──────────────────────────────────┘              │
│                                                      │
│   ┌──────────────────────────────────┐              │
│   │   Webhooks (near-real-time)      │              │
│   │   Invalidates cache + upserts    │              │
│   └──────────────────────────────────┘              │
└─────────────────────────────────────────────────────┘

Database Schema

connector_resource

Stores synced resource records from upstream connectors.

Column Type Description
id UUID PK Auto-generated
connector_id UUID FK References connector_definition
resource_type VARCHAR(64) user, group, role, etc.
external_id VARCHAR(255) ID from the upstream system
display_name VARCHAR(255) Human-readable name
email VARCHAR(255) Optional email address
attributes JSONB Arbitrary upstream attributes
sync_hash VARCHAR(64) SHA-256 of the canonical record
stale_since TIMESTAMPTZ Set when the record was not refreshed during a sync pass
synced_at TIMESTAMPTZ Last time the record was confirmed upstream
created_at TIMESTAMPTZ Row creation time

A unique constraint on (connector_id, resource_type, external_id) prevents duplicates.

connector_sync_config

Stores per-connector, per-resource-type synchronization configuration.

Column Type Description
id UUID PK Auto-generated
connector_id UUID FK References connector_definition
resource_type VARCHAR(64) Resource type this config applies to
enabled BOOLEAN Whether scheduled sync is active
strategy VARCHAR(32) full or incremental
cron_schedule VARCHAR(64) Cron expression for scheduled syncs
filter_rules JSONB Optional filter rules (see below)
stale_retention VARCHAR(32) How long to keep stale records (e.g. 7d, 24h)
last_sync_at TIMESTAMPTZ Timestamp of last sync completion
last_sync_cursor TEXT Resume cursor on error
last_sync_status VARCHAR(32) idle, running, success, error
last_sync_error TEXT Error message if last sync failed
last_sync_stats JSONB Statistics from the last sync run
attribute_mappings JSONB Declarative mappings from connector resources to profile fields (see Attribute mapping)
user_match_strategy VARCHAR(32) Primary strategy: email, email_and_issuer, upstream_identity, or external_id (default email)
user_match_fallback_strategies JSONB Ordered list of additional strategies tried when the primary finds no unique user
issuer_source_path TEXT Dot-path to issuer field on resource (for email_and_issuer); defaults to attributes.identityIssuer at runtime
post_sync_workflow_id UUID FK Optional workflow_definition to run after sync (nullable; ON DELETE SET NULL)
sync_trigger_mode VARCHAR(32) How post-sync workflow automation is scoped: none, per_record, or summary (default none)
create_users BOOLEAN When true, post-sync may create Floh users for rows that remain unmatched (default false)
deactivate_users BOOLEAN When true, post-sync may soft-delete users whose matched connector row has gone stale (default false)
create_user_defaults JSONB Profile attribute payload applied to users auto-created via create_users

connector_sync_match

One row per synced connector resource (for configured user resource types), used to reconcile upstream identities to Floh users and drive profile updates.

Column Type Description
id UUID PK Auto-generated
connector_id VARCHAR(36) FK References connector_definition
resource_type VARCHAR(64) Same resource type as the sync config (e.g. user)
connector_resource_id UUID FK References connector_resource
external_id VARCHAR(255) Upstream identifier
external_display_name VARCHAR(255) Cached display name
external_email VARCHAR(255) Cached email
user_id UUID FK Linked Floh user when matched or resolved (nullable)
match_status VARCHAR(32) matched, unmatched, ambiguous, manual_override, skipped, create_pending
match_confidence VARCHAR(32) exact, probable, ambiguous, or none
match_strategy VARCHAR(32) Strategy that produced the current outcome, when applicable
candidate_user_ids JSONB Candidate list when status is ambiguous
resolution_notes TEXT Optional operator notes
resolved_by UUID FK User who resolved the row
resolved_at TIMESTAMPTZ Resolution timestamp
created_at / updated_at TIMESTAMPTZ Row metadata

Unique on (connector_id, resource_type, connector_resource_id).

Sync Strategies

Full Sync

A full sync fetches all resources from the upstream connector, upserts them into connector_resource, and marks any records that were not seen during this pass as stale. Stale records are purged after the retention period.

Best for: Reference data that should always reflect the upstream truth (groups, roles).

Incremental Sync

An incremental sync requests only records modified since the last successful sync (modifiedSince parameter). It does not mark unseen records as stale — since it only fetches changes, absence does not imply deletion.

Best for: Large datasets where full enumeration is expensive and the upstream supports modification timestamps.

Filter Rules

Filter rules restrict which upstream records are synced. They are stored in the filter_rules JSONB column and applied in-memory after each page is fetched.

interface SyncFilterRules {
  groupNamePattern?: string; // Glob-like pattern (e.g. "proj-*")
  groupIds?: string[]; // Whitelist of specific group IDs
  emailDomains?: string[]; // Only sync users from these domains
  memberOfSyncedGroups?: boolean; // Only sync users in already-synced groups
  maxRecords?: number; // Hard cap on total synced records
}

Group Filters

  • groupNamePattern — Matches the displayName using glob syntax (* = any chars, ? = single char). Case-insensitive.
  • groupIds — Explicit whitelist; only groups with matching externalId are synced.
  • Both filters compose with AND: a record must pass all specified filters.

User Filters

  • emailDomains — Only syncs users whose email domain matches one of the listed domains. Case-insensitive.
  • maxRecords — Stops syncing after this many records have been processed (across all pages).

Sync Hash and Change Detection

Each upstream record is serialized into a canonical JSON form:

{
  "externalId": "...",
  "displayName": "...",
  "email": "..." | null,
  "attributes": { ... }
}

A SHA-256 hash of this JSON is computed and stored as sync_hash. On subsequent syncs, the hash is compared:

  • Match → Record is unchanged; only synced_at is updated.
  • Mismatch → Record is updated with new data and hash.
  • New → Record is inserted.

This minimizes database writes for large datasets where most records haven't changed.

Stale Management

After a full sync completes, any records whose synced_at is older than the sync start time (meaning they were not seen upstream) are marked stale by setting stale_since to the current time.

Stale records are retained for a configurable period (default 7d) before being purged. This protects against transient upstream issues — if a group temporarily disappears from an API response, it won't be immediately deleted.

Redis Cache (Tier 2)

The ConnectorCacheService provides a TTL-based Redis cache for individual resource lookups. This is used during workflow execution to avoid repeated API calls when checking a single user or group.

Key format: cr:{connectorId}:{resourceType}:{externalId}

Default TTL: 300 seconds (5 minutes)

Cache-Aside Pattern

getOrFetch(connectorId, type, externalId, fetcher):
  1. Check Redis → if hit, return cached value
  2. Call fetcher() → if null, return null
  3. Store result in Redis with TTL
  4. Return result

Cache Invalidation

Cache entries are invalidated:

  • On webhook events (created/updated/deleted)
  • On manual cache clear
  • Automatically via TTL expiry

Webhook Integration

The webhook handler at POST /api/webhooks/:connectorId has been extended to handle resource sync events:

Action Cache DB
created Invalidate key Upsert record with hash
updated Invalidate key Upsert record with hash
deleted Invalidate key Delete record

The webhook body must include a resourceType field to trigger resource sync handling. Without it, the event falls through to the existing entitlement reconciliation handler.

{
  "action": "updated",
  "resourceId": "user-123",
  "resourceType": "user",
  "data": {
    "displayName": "Jane Doe",
    "email": "jane@acme.com"
  }
}

Post-sync processing

After a sync run finishes writing connector_resource rows, post-sync processing may run. It reconciles users, optionally creates or deactivates accounts, applies attribute mappings to profiles, and can feed post-sync workflow automation.

Post-sync runs only when the sync pass had at least one added or updated resource (not on a purely unchanged run). Failures in post-sync are logged and do not fail the overall sync status.

The pipeline (in order):

  1. User matching — Recompute connector_sync_match rows from current resources and Floh users.
  2. Auto-create users — If createUsers is enabled, create minimal unconfirmed users for rows still unmatched that have an email, then link the match.
  3. Auto-deactivation — If deactivateUsers is enabled, soft-delete users who are matched only through this connector/resource type and whose linked resource has stale_since set (and who have no other sync match rows on other connectors).
  4. Attribute mapping — For matches in matched or manual_override with a user_id, apply configured mappings into the user profile.

User matching strategies

Matching compares each synced resource to v_user using a primary strategy (userMatchStrategy) and an optional fallback chain (userMatchFallbackStrategies). Duplicates in the fallback list are ignored; the primary is always tried first.

Strategy Connector side Floh user field
email Normalized resource email email (case-insensitive)
email_and_issuer Normalized email + issuer from configurable dot-path email + upstream_issuer (compound key)
upstream_identity Resource externalId upstream_id
external_id Resource externalId sub

The email_and_issuer strategy resolves the issuer from the resource using issuerSourcePath (defaults to attributes.identityIssuer when not set). The compound key is lower(email)|lower(issuer). This is useful in multi-IdP environments where the same email may appear under different identity providers.

For each strategy in order, the engine looks up users by that key. No match moves to the next strategy. Exactly one user produces a match for that step. Two or more users stop the chain with status ambiguous and populate candidate_user_ids.

All lookup queries use the withTempLookup helper (db/temp-lookup.ts) which inserts lookup keys into a temporary table and uses WHERE column IN (SELECT value FROM _lk_...) instead of parameterized IN (...) clauses. This avoids PostgreSQL's ~65,535 parameter limit and produces efficient query plans for large directory syncs.

Match confidence

Confidence Meaning
exact Unique hit on the primary strategy
probable Unique hit on a fallback strategy
ambiguous Multiple users matched for the current strategy
none No user matched after exhausting the chain

Match reconciliation

Operators resolve ambiguous rows or correct bad links via the Match reconciliation API: link to a specific userId, mark a row as skipped, unlink to reset to unmatched, or use create (single-row resolve) to create an unconfirmed user from the cached resource email and link. Resolved rows can move to manual_override or matched depending on the action.

Attribute mapping

attributeMappings on the sync config is a declarative list of rules that copy values from a connector resource (after sync) into user profile fields. Each entry includes:

Field Description
sourcePath Dot path into the resource, rooted at top-level fields and attributes (e.g. email, attributes.departmentCode). Reads use own-property rules and block unsafe path segments.
targetField Profile field name: built-in keys such as title, department, phoneNumber, location, employeeId, costCenter, startDate, or a custom attribute name.
writeMode How to treat an existing profile value (see below).
transform Reserved for future expression-based transforms; non-empty values are currently skipped.
enabled When false, the rule is ignored.

Write modes:

Mode Behavior
overwrite_always Write the source value whenever it is present on the resource.
overwrite_if_empty Write only when the current profile value is empty (null, undefined, or blank string).
never_overwrite Never write; useful for keeping a rule disabled without deleting it.

Updates go through ProfileRepository.updateAttributes with source connector. The profile DTO’s attributeSources map records which fields were last written by manual, oidc, workflow, or connector, so UIs and policies can distinguish connector-driven values.

Workflow variables: Steps that resolve the subject user’s profile (or variables derived from it) see values produced by mapping on the next run after a successful post-sync pass. Connector data does not bypass the profile layer—sync → match → profile update → workflows consume the same profile model as the rest of Floh.

User lifecycle management

Option Default Behavior
createUsers false After matching, for each unmatched row with a non-empty external_email, create an unconfirmed Floh user, link the match, and set confidence to exact. Rows without email are skipped.
deactivateUsers false After matching, soft-delete users who are matched only through this connector/resource pair and whose resource row is stale (stale_since set). Users with sync matches on other connectors are not deactivated.
createUserDefaults null JSON object of profile fields applied once to each user created via createUsers (same shape as profile attribute updates, stored with source connector).

Email collisions during auto-create are handled as errors for that row; the resolve API’s create action similarly returns a conflict if the email already exists.

Post-sync workflow triggers

Sync configuration can reference a postSyncWorkflowId (a workflow_definition id) together with syncTriggerMode:

Mode Role
none No post-sync workflow automation (default).
per_record Intended for automation scoped to individual changed records; job payloads may include a records collection for the workflow.
summary Intended for a single post-sync run with aggregate context; job payloads may include a stats object.

The integrations worker registers a sync-workflow-trigger job handler. Job data is validated to include connectorId, resourceType, workflowId, and triggerMode (the configured mode). Optional records and stats are passed through when present. The handler resolves the workflow definition and builds the initial variable bag:

Variable Description
connectorId Connector instance id
resourceType Synced resource type
workflowId Target workflow definition id
triggerMode Same as syncTriggerMode
records Present when the dispatcher sends changed-record detail
stats Present when the dispatcher sends aggregate post-sync / sync stats

Exact enqueue timing and payload shape for each mode are defined by the dispatcher that enqueues sync-workflow-trigger jobs; the sync config fields store the operator’s chosen workflow and mode.

Match reconciliation API

All paths are under /api/connectors and require authentication.

List sync matches

GET /api/connectors/:id/sync-matches?type=user&page=1&pageSize=20&status=ambiguous&search=acme

Query parameters

Parameter Required Description
type Yes Resource type (e.g. user)
status No Filter: matched, unmatched, ambiguous, manual_override, skipped, create_pending
search No Search term for external id, email, or display name
page, pageSize No Pagination (same conventions as other list APIs; pageSize up to 2000)

Response: Paginated list of match records (data, total, page, pageSize, totalPages).

Example item:

{
  "id": "b2c0c4d8-…",
  "connectorId": "…",
  "resourceType": "user",
  "connectorResourceId": "…",
  "externalId": "upstream-42",
  "externalDisplayName": "Jane Doe",
  "externalEmail": "jane@acme.com",
  "userId": null,
  "matchStatus": "ambiguous",
  "matchConfidence": "ambiguous",
  "matchStrategy": "email",
  "candidateUserIds": [
    {
      "userId": "…",
      "email": "jane@acme.com",
      "confidence": "ambiguous",
      "reason": "email"
    }
  ],
  "resolutionNotes": null,
  "resolvedBy": null,
  "resolvedAt": null,
  "createdAt": "…",
  "updatedAt": "…"
}

Sync match statistics

GET /api/connectors/:id/sync-matches/stats?type=user

Response:

{
  "matched": 120,
  "unmatched": 3,
  "ambiguous": 2,
  "skipped": 0
}

Resolve a sync match

POST /api/connectors/:id/sync-matches/:matchId/resolve
Content-Type: application/json

{
  "action": "link",
  "userId": "uuid-of-floh-user",
  "notes": "Verified with HR"
}

Body

action userId Behavior
link Required Attach the match to the given Floh user
skip Mark the row skipped (no user link)
create Create an unconfirmed user from the cached resource email and link (fails with 400 if email missing, 409 if email exists)

Response: Updated match record (200).

POST /api/connectors/:id/sync-matches/:matchId/unlink

Clears the user link and resolution metadata so the row can be matched again on a later post-sync pass.

Response: Updated match record (200).

Bulk resolve sync matches

POST /api/connectors/:id/sync-matches/bulk-resolve
Content-Type: application/json

{
  "matchIds": ["uuid-1", "uuid-2"],
  "action": "link",
  "userId": "uuid-of-floh-user",
  "notes": "Bulk link after directory review"
}

Body: matchIds (non-empty array), action link or skip. userId is required when action is link. Only matches belonging to the connector :id are updated.

Response:

{ "updated": 2 }

API Reference

All endpoints require authentication and are scoped under /api/connectors.

List Synced Resources

GET /api/connectors/:id/resources?type=group&page=1&pageSize=20&search=eng

Returns paginated resources from the local sync cache. Supports filtering by type, search term, and stale status.

List Sync Configurations

GET /api/connectors/:id/sync-config

Returns all sync configurations for a connector.

Get Sync Configuration

GET /api/connectors/:id/sync-config/:type

Returns the sync configuration for a specific resource type.

Create or Update Sync Configuration

PUT /api/connectors/:id/sync-config/:type
Content-Type: application/json

{
  "enabled": true,
  "strategy": "full",
  "cronSchedule": "0 */6 * * *",
  "filterRules": {
    "groupNamePattern": "proj-*"
  },
  "staleRetention": "7d",
  "userMatchStrategy": "email_and_issuer",
  "issuerSourcePath": "attributes.identityIssuer",
  "userMatchFallbackStrategies": ["email"],
  "attributeMappings": [
    {
      "sourcePath": "attributes.department",
      "targetField": "department",
      "writeMode": "overwrite_if_empty",
      "enabled": true
    },
    {
      "sourcePath": "displayName",
      "targetField": "title",
      "writeMode": "overwrite_always",
      "enabled": true
    }
  ],
  "createUsers": false,
  "deactivateUsers": false,
  "createUserDefaults": {
    "location": "Remote"
  },
  "postSyncWorkflowId": null,
  "syncTriggerMode": "none"
}

Creates a new sync configuration or updates an existing one. When enabled with a cron schedule, a BullMQ repeatable job is registered automatically. On update, include only fields to change. On create, omitted keys use the defaults in the table below.

Sync configuration fields

Field (API) Default (create) Description
enabled false Turns scheduled sync on or off
strategy full full or incremental
cronSchedule null Cron for repeatable sync jobs
filterRules null In-memory filters (see Filter rules)
staleRetention 7d Stale purge window
attributeMappings null List of attribute mapping rules
userMatchStrategy email Primary match strategy
userMatchFallbackStrategies null Extra strategies in order after the primary
issuerSourcePath null Dot-path for issuer on resource (for email_and_issuer; defaults to attributes.identityIssuer at runtime when null)
postSyncWorkflowId null Workflow definition UUID for post-sync automation
syncTriggerMode none none, per_record, or summary
createUsers false Auto-create users for unmatched rows with email
deactivateUsers false Soft-delete users tied only to stale matches on this connector
createUserDefaults null Profile patch applied to auto-created users

Delete Sync Configuration

DELETE /api/connectors/:id/sync-config/:type

Removes the sync configuration and its associated scheduled job.

Trigger Manual Sync

POST /api/connectors/:id/sync-config/:type/trigger

Immediately runs a sync for the specified resource type. Returns the sync statistics on completion.

Response:

{
  "message": "Sync completed",
  "stats": {
    "added": 12,
    "updated": 3,
    "staled": 0,
    "unchanged": 485,
    "removed": 0,
    "durationMs": 8420,
    "pagesProcessed": 1,
    "totalUpstreamRecords": 500
  }
}

Get Sync Status

GET /api/connectors/:id/sync-config/:type/status

Returns the last sync status, error, and statistics without the full configuration.

Connector Implementation

For a connector to support resource sync, it must implement paginated list commands. The connector's configSchema should declare these commands with syncCapable: true.

Required Commands

Resource Type Command Description
group listGroups Paginated group listing
user listUsers Paginated user listing
role listRoles Paginated role listing

Command Interface

Each list command receives:

{
  command: 'listGroups',
  cursor?: string,       // Resume cursor from previous page
  pageSize?: number,     // Requested page size (default 500)
  filter?: object,       // Optional filter from sync config
  modifiedSince?: string // ISO timestamp for incremental sync
}

And must return:

{
  success: true,
  payload: {
    resources: [
      {
        externalId: 'grp-123',
        displayName: 'Engineering',
        email: null,
        attributes: { description: 'Engineering team', memberCount: 45 }
      }
    ],
    nextCursor: 'page-2-token',  // undefined when no more pages
    totalEstimate: 150           // optional, for progress indication
  }
}

Authifi Example

The Authifi connector implements listGroups and listUsers out of the box. Both commands use cursor-based pagination and map upstream API responses to the standard ConnectorResourceRecord format.

Background Scheduling

When a sync configuration is enabled with a cronSchedule, a BullMQ repeatable job is registered with the id cr-sync-{connectorId}-{resourceType}. The worker handler invokes ConnectorSyncService.runSync().

On application startup, all enabled sync configs are loaded from the database and their corresponding jobs are registered with the scheduler. This ensures schedules survive server restarts.

Permissions

Endpoint Required Permission
GET .../resources connector:read
GET .../sync-config connector:read
GET .../sync-config/:type/status connector:read
PUT .../sync-config/:type connector:manage
DELETE .../sync-config/:type connector:manage
POST .../sync-config/:type/trigger connector:manage
GET .../sync-matches connector:read
GET .../sync-matches/stats connector:read
POST .../sync-matches/:matchId/resolve connector:manage
POST .../sync-matches/:matchId/unlink connector:manage
POST .../sync-matches/bulk-resolve connector:manage

Shared Types

All sync-related types are defined in packages/shared/src/connector-sync.types.ts and exported from @floh/shared. Key interfaces:

  • ConnectorResourceRecord — Upstream resource shape
  • ConnectorListResult — Paginated list response from connectors
  • ConnectorSyncConfigDto — Sync configuration DTO
  • SyncFilterRules — Filter rule schema
  • SyncStats — Sync run statistics
  • PostSyncStats — Post-sync run aggregates (users created/deactivated, profiles updated, match stats)
  • AttributeMapping / AttributeWriteMode — Declarative profile mapping rules
  • UserMatchStrategyemail | email_and_issuer | upstream_identity | external_id
  • SyncTriggerModenone | per_record | summary
  • MatchConfidence, MatchStatus, MatchCandidate, SyncMatchRecordDto — Reconciliation model
  • ResolveMatchDto, BulkResolveMatchDto — Match API payloads
  • ConnectorResourceDto — Resource as returned by the API
  • ResourceWebhookEvent — Webhook event for resource changes

Profile DTOs (UserProfileDto, ProfileAttributeSource) in packages/shared/src/profile.types.ts describe attributeSources used with connector-driven updates.

File Layout

packages/shared/src/
  connector-sync.types.ts         # Shared type definitions
  profile.types.ts                # Profile DTOs and attribute source enum

packages/server/src/
  db/migrations/
    003_connector_resource_sync.ts  # Initial resource sync tables
    012_sync_profile_integration.ts # Sync config extensions + connector_sync_match
    013_compound_match_strategy.ts  # issuer_source_path column for email_and_issuer
  db/
    temp-lookup.ts                 # withTempLookup helper for large IN-clause replacement
  db/schema/
    operational-tables.ts          # Table type definitions
    database.ts                    # Database interface registration
  modules/connectors/
    sync-repository.ts             # ConnectorResourceRepository, ConnectorSyncConfigRepository
    sync-service.ts                # ConnectorSyncService (orchestration)
    sync-routes.ts                 # API routes for sync management
    sync-match-*.ts                # Sync match routes, schemas, repository, service, runner
    sync-post-processor*.ts        # Post-sync pipeline (match, users, attributes, lifecycle)
    connector-resource-dto-mapper.ts
    connector-cache.ts             # ConnectorCacheService (Redis)
    authifi-client.ts              # Extended with listGroupsPaginated, listUsersPaginated
    authifi-connector.ts           # Extended with listGroups, listUsers commands
    seed-connectors.ts             # Updated schema with syncCapable commands
  modules/roles/routes/
    webhook-routes.ts              # Extended for resource webhook events
  modules/scheduler/handlers/
    sync-workflow-trigger.ts       # Post-sync workflow job handler (integrations queue)
  route-registry.ts               # Sync + sync-match routes registration
  app.ts                           # Startup job scheduling
  worker-handlers.ts               # Sync job handler
  worker.ts                        # Worker deps with sync service

packages/server/test/unit/
  connector-cache.test.ts                # Cache service tests
  connector-sync-repository.test.ts      # Repository tests
  connector-sync-service.test.ts         # Sync orchestration tests
  connector-sync-routes.test.ts          # API route tests
  connector-sync-filter.test.ts          # Filter rules engine tests
  authifi-connector-sync.test.ts         # Authifi list commands tests
  connector-resource-webhook.test.ts     # Webhook extension tests
  sync-match-mappers.test.ts             # Match DTO / row mapping
  sync-post-processor-mapping.test.ts    # Attribute mapping behavior
  temp-lookup.test.ts                    # Temp table lookup utility tests