# How Plaid Integration Syncs Bank Account Data in Maybe Finance: A Technical Deep Dive

> Discover how Plaid integration syncs bank account data in Maybe Finance. Learn about the three-stage pipeline import process and asynchronous Sidekiq job execution.

- Repository: [Maybe/maybe](https://github.com/maybe-finance/maybe)
- Tags: deep-dive
- Published: 2026-03-07

---

**The Plaid integration in Maybe Finance uses a generic sync framework that orchestrates bank account data synchronization through a three-stage pipeline—import, process, and schedule—executed asynchronously via Sidekiq jobs.**

Maybe Finance treats every data source as a *syncable* object within a unified framework. When syncing bank account data via Plaid, the system leverages a sophisticated pipeline that abstracts the complexity of the Plaid API behind a consistent interface. This article examines how the Plaid integration syncs bank account data by analyzing the actual source code from the `maybe-finance/maybe` repository.

## The Generic Sync Framework Architecture

The Plaid integration builds upon a reusable sync infrastructure defined in the `Syncable` concern and the `Sync` model. This architecture ensures that whether the data comes from Plaid, CSV imports, or market data providers, the synchronization lifecycle remains consistent.

### The Syncable Concern

The `Syncable` concern in [`app/models/concerns/syncable.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/concerns/syncable.rb) provides the entry point for all synchronization operations. It adds the `sync_later` method to any model that includes it, which creates or expands a `Sync` record and enqueues a `SyncJob` for asynchronous processing.

When `plaid_item.sync_later` is called, the concern checks for existing pending syncs and either expands their time window or creates a new `Sync` record. This prevents duplicate syncs while ensuring that rapid changes are captured within a single synchronization window.

### The Sync State Machine

The `Sync` model in [`app/models/sync.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/sync.rb) implements a state machine that manages the synchronization lifecycle. When `SyncJob` invokes `Sync#perform`, the model transitions from **pending** to **syncing** and calls `syncable.perform_sync(self)`.

The state machine handles completion, failure, and child sync relationships. When child syncs finish, `Sync#finalize_if_all_children_finalized` checks if all dependent syncs are complete before marking the parent sync as finished and broadcasting completion events.

## How Plaid Integration Syncs Bank Account Data: The Three-Stage Pipeline

When a `PlaidItem` executes its synchronization, it delegates to `PlaidItem::Syncer` in [`app/models/plaid_item/syncer.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item/syncer.rb). This class orchestrates a three-stage pipeline that separates data fetching from account processing and historical synchronization.

### Stage 1: Importing Raw Plaid Data

The first stage calls `PlaidItem#import_latest_plaid_data`, which instantiates `PlaidItem::Importer` to fetch fresh data from the Plaid API. The importer retrieves item metadata, institution information, and account listings using the low-level client defined in `Provider::Plaid`.

During import, the system stores raw Plaid payloads using `upsert_plaid_snapshot!` and `upsert_plaid_institution_snapshot!` methods on the `PlaidItem` model. These snapshots persist the API responses in [`app/models/plaid_item.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item.rb), enabling debugging and UI display of linked account details without requiring additional API calls.

### Stage 2: Processing Accounts

After importing raw data, `PlaidItem#process_accounts` iterates over each `PlaidAccount` associated with the item and runs its processor. This step transforms the raw Plaid account data into the internal `Account` model representation used by Maybe Finance.

The processing step handles account metadata updates, balance normalization, and currency conversion preparation. Each `PlaidAccount` processes its specific data type, whether checking, savings, credit, or investment accounts, ensuring that the internal state reflects the latest information from the financial institution.

### Stage 3: Scheduling Account-Level Syncs

The final stage calls `PlaidItem#schedule_account_syncs`, which creates child `Sync` records for every associated `Account`. These child syncs handle historical balance calculations, exchange rate computations, and transaction categorization separately from the main Plaid data import.

By decoupling the Plaid API sync from account-level historical processing, the system allows for more granular error handling and retry logic. If historical balance calculation fails for one account, it does not mark the entire Plaid connection as failed, and other accounts can continue processing independently.

## Fetching Data from the Plaid API

The actual communication with Plaid's servers is abstracted behind specific classes that handle authentication, endpoint selection, and response pagination.

### The Provider::Plaid Client

The `Provider::Plaid` class in [`app/models/provider/plaid.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/provider/plaid.rb) serves as the low-level API client. It wraps the official Plaid SDK and provides methods for creating link tokens, exchanging public tokens for access tokens, and fetching accounts, transactions, investments, and liabilities.

This provider class centralizes authentication logic, ensuring that the access token stored in the `PlaidItem` model is correctly passed to every API request. It also handles error translation, converting Plaid-specific exceptions into the internal error handling framework used by Maybe Finance.

### Handling Pagination and Cursors

For endpoints that return large datasets, such as transactions, the `PlaidItem::AccountsSnapshot` class in [`app/models/plaid_item/accounts_snapshot.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item/accounts_snapshot.rb) manages pagination and cursor state. This wrapper calls the appropriate Plaid endpoints—including `get_item_accounts`, `get_transactions`, `get_item_investments`, and `get_item_liabilities`—and memoizes responses.

The snapshot class handles cursor management for transaction history, ensuring that only new or updated transactions are fetched on subsequent syncs rather than retrieving the entire history each time. This optimization reduces API call volume and improves sync performance for accounts with extensive transaction histories.

## Code Examples

Trigger a sync for the first Plaid connection of a family:

```ruby
family = Family.find(1)
plaid_item = family.plaid_items.first
plaid_item.sync_later   # => creates a Sync record and enqueues a SyncJob

```

Inspect the sync status after the job processes:

```ruby
sync = plaid_item.syncs.last
sync.status             # => "completed"

sync.created_at         # sync start time

sync.completed_at       # set when the sync finishes

```

Manually run the sync steps for debugging:

```ruby
plaid_item.import_latest_plaid_data   # runs PlaidItem::Importer

plaid_item.process_accounts           # updates PlaidAccount objects

plaid_item.schedule_account_syncs     # schedules per-account historical syncs

```

Inspect the raw payloads stored during sync:

```ruby
plaid_item.raw_payload                # => JSON representation of the Plaid item

plaid_item.raw_institution_payload    # => institution metadata from Plaid

```

## Key Files in the Plaid Integration

| Component | File | Role |
|---|---|---|
| Plaid API client | [`app/models/provider/plaid.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/provider/plaid.rb) | Thin wrapper around Plaid SDK; creates link tokens, exchanges tokens, fetches accounts, transactions, investments, liabilities |
| Plaid connection model | [`app/models/plaid_item.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item.rb) | Holds access token, tracks status, implements `sync_later`, imports and processes data |
| Sync orchestrator for PlaidItem | [`app/models/plaid_item/syncer.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item/syncer.rb) | Coordinates import, processing, and child-account sync scheduling |
| Plaid data importer | [`app/models/plaid_item/importer.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item/importer.rb) | Pulls item, institution, and accounts data from Plaid and persists raw payloads |
| Account-level snapshot helper | [`app/models/plaid_item/accounts_snapshot.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item/accounts_snapshot.rb) | Handles pagination, cursors, and product-specific data (transactions, investments, liabilities) |
| Generic sync concern | [`app/models/concerns/syncable.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/concerns/syncable.rb) | Adds `sync_later` and hooks into the `Sync` state machine |
| Sync model and state machine | [`app/models/sync.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/sync.rb) | Stores sync records, manages lifecycle (pending → syncing → completed/failed), expands windows, cleans stale syncs |
| Background job | [`app/jobs/sync_job.rb`](https://github.com/maybe-finance/maybe/blob/main/app/jobs/sync_job.rb) | Executes `Sync#perform` asynchronously via Sidekiq |
| Plaid item controller | [`app/controllers/plaid_items_controller.rb`](https://github.com/maybe-finance/maybe/blob/main/app/controllers/plaid_items_controller.rb) | Endpoints for creating, updating, and deleting Plaid connections (calls `sync_later` indirectly) |

## Summary

- The Plaid integration in Maybe Finance builds on a **generic sync framework** that treats all data sources as syncable objects, ensuring consistent handling across Plaid, CSV imports, and market data.
- Synchronization follows a **three-stage pipeline**: importing raw Plaid data via `PlaidItem::Importer`, processing accounts through `PlaidItem#process_accounts`, and scheduling child syncs for historical balance calculations.
- The `Syncable` concern in [`app/models/concerns/syncable.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/concerns/syncable.rb) provides the entry point `sync_later`, which creates `Sync` records and enqueues `SyncJob` for asynchronous processing via Sidekiq.
- Raw Plaid payloads are persisted in the `PlaidItem` model using `upsert_plaid_snapshot!` and `upsert_plaid_institution_snapshot!`, enabling debugging and UI display without additional API calls.
- Pagination and cursor management for large datasets like transaction history are handled by `PlaidItem::AccountsSnapshot` in [`app/models/plaid_item/accounts_snapshot.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item/accounts_snapshot.rb), optimizing API usage by fetching only new or updated records.

## Frequently Asked Questions

### How does the Plaid integration handle authentication tokens?

The `PlaidItem` model stores the Plaid access token securely. When the system needs to fetch data, the `Provider::Plaid` class in [`app/models/provider/plaid.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/provider/plaid.rb) uses this token to authenticate requests to the Plaid API. The token is obtained during the initial link flow when users connect their accounts, and it is never exposed to the client-side application after creation.

### What happens if a Plaid sync fails?

The `Sync` model in [`app/models/sync.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/sync.rb) implements a state machine that tracks sync status through pending, syncing, completed, and failed states. If a sync fails, the error is captured in the sync record, and Sidekiq's retry mechanism attempts to reprocess the job according to the configured retry policy. Because the system uses child syncs for account-level processing, failures in historical balance calculations are isolated to specific accounts without failing the entire Plaid connection.

### How does Maybe Finance handle large transaction histories?

The `PlaidItem::AccountsSnapshot` class in [`app/models/plaid_item/accounts_snapshot.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/plaid_item/accounts_snapshot.rb) manages pagination and cursor state for endpoints like `get_transactions`. Instead of fetching the entire transaction history on every sync, the system uses Plaid's cursor-based pagination to retrieve only new or updated transactions since the last sync. This optimization reduces API call volume and improves performance for accounts with extensive transaction histories.

### Can developers manually trigger a Plaid sync for testing?

Yes, developers can manually trigger synchronization by calling the `sync_later` method on any `PlaidItem` instance, which is defined in the `Syncable` concern at [`app/models/concerns/syncable.rb`](https://github.com/maybe-finance/maybe/blob/main/app/models/concerns/syncable.rb). For debugging specific stages, developers can also manually invoke `import_latest_plaid_data`, `process_accounts`, and `schedule_account_syncs` directly on the `PlaidItem` model to step through the three-stage pipeline without enqueuing background jobs.