Created: 2026-01-28
Updated: 2026-01-29
Status: 📋 Planned
Owner: FA (Finance & Accounting)
Overview
This document defines the integration contract between Encore OS’s FA module and Plaid for automated bank feed integration. Plaid provides broad institution coverage (11,000+ globally), OAuth-based authentication, and a developer-friendly API.Integration Pattern
Type: External API Integration via Edge FunctionsDirection: Outbound (Encore OS → Plaid) + Inbound (Webhooks)
Authentication: API Keys (client_id + secret)
Authentication Requirements
Environment Matrix
Credentials
API Endpoints
Plaid API (Outbound)
Edge Functions (Internal)
Transfer UI (One-Time Payment / Disbursement)
Transfer UI is Plaid’s hosted flow for one-time debit or credit. It is not used for batch payroll (see Programmatic Plaid Transfer below). Flow:- Create intent: Frontend calls
plaid-transfer-intent-createwithorganization_id,mode(PAYMENT | DISBURSEMENT),amount,description,ach_class,user(e.g.legal_name). Backend calls PlaidPOST /transfer/intent/createand returnstransfer_intent_id. - Link token: Frontend calls
plaid-create-link-tokenwithorganization_id,transfer_intent_id, and optionallink_customization_name. Backend returns a link token withproducts: ["transfer"]andtransfer.intent_id. - Open Link: Frontend opens Plaid Link with that token; user authorizes the transfer in Plaid’s UI.
- Post-success: On Link
onSuccess,metadata.transfer_statusis available; frontend may callplaid-transfer-intent-getwithtransfer_intent_idto get finalstatus,transfer_id, andfailure_reason.
- Origination account: Required for transfer intent. Resolved in order: (1)
fa_module_settings.plaid_origination_account_idfor the organization, (2) envPLAID_ORIGINATION_ACCOUNT_ID. At least one must be set. - Link customization: In Plaid Dashboard, create a Link customization with Account Select: Enabled for one account. Optionally set
fa_module_settings.plaid_link_customization_nameper org, or passlink_customization_namewhen requesting the link token / from frontend (useTransferUI/TransferUIButton).
useTransferUI and TransferUIButton from @/platform/banking; they orchestrate intent create → link token → Link open → optional intent/get.
Programmatic Plaid Transfer (HR Payroll)
Payroll direct deposit uses Plaid Transfer in a two-step flow. Per Plaid: Creating transfers:- Authorize: Call
POST /transfer/authorization/createwithaccess_token,account_id,type,network,amount,ach_class,user, and an idempotency_key. Plaid returnsauthorization_idanddecision(approved/declined). - Create: If approved, call
POST /transfer/createwithauthorization_id,access_token,account_id, anddescriptiononly.
hr-plaid-transfer-create edge function uses the shared plaid-client.ts (createTransferAuthorization, createTransfer) and passes an idempotency key per payment item (e.g. hr-pay-{batchId}-{itemId}) to avoid duplicate transfers on retries. ACH description is limited to 10 characters (e.g. PAY + batch number suffix).
Data Flow
Webhook Events
Supported Events
Signature Verification
Header:Plaid-Verification
The webhook body is signed using JWT. Verification requires:
- Fetch Plaid’s verification key
- Verify JWT signature
- Validate claims (iat, request_body_sha256)
Database Schema
fa_bank_accounts (Additional Columns)
fa_bank_statement_lines (Additional Column)
fa_module_settings (Additional Columns)
Security Considerations
Token Storage
- Access tokens stored in
plaid_access_tokencolumn (encrypted at rest by Supabase) - Tokens never exposed to frontend
- Edge functions access tokens via service role
Multi-Tenancy
- All Edge Function queries include
organization_idfilter - RLS policies prevent cross-tenant access
- Audit logging for all Plaid operations
Webhook Security
- JWT signature verification
- Request body hash validation
- Rate limiting via Supabase
Institution-specific behavior (Duplicate Items)
For Chase, PNC, Navy Federal Credit Union, and Charles Schwab, linking the same credentials again (creating a second Plaid Item for the same institution) can invalidate the existing Item at the time the user completes the institution’s OAuth flow. When this occurs:- Option 1: Use Plaid’s update mode to repair the existing Item (re-launch Link with the existing
access_token). That will invalidate the new Item; then remove the new Item via/item/removeif it was already created. - Option 2: Remove the old Item using Plaid’s
/item/removeendpoint and use the new Item.
plaid_institution_id before exchanging the public token (see duplicate prevention in implementation). This runbook is for awareness when handling support cases or when adding Item-level health handling (e.g. webhooks) that may replace an invalidated Item.
Error Handling
Plaid API Errors
Retry Strategy
Logging
Allowlisted Fields (Safe to Log)
bank_account_idplaid_item_id(not access token)plaid_account_idtransactions_syncedsync_duration_ms
Prohibited (Never Log)
plaid_access_tokenplaid_secret- Full account numbers
Testing Strategy
Sandbox Testing
- Use Plaid sandbox environment
- Test institutions:
ins_109508(Chase),ins_109511(Bank of America) - Test credentials:
user_good/ any password
Integration Tests
- Link token creation
- Token exchange
- Transaction sync with deduplication
- Webhook signature verification
E2E Tests
- Full connect → sync → disconnect flow
Rollback Strategy
All Plaid columns are nullable. Rollback procedure:- Disable feature flag:
plaid_integration_enabled = false - UI hides all Plaid components
- Manual CSV/OFX import continues to work
- No data migration required
Account selection and GL mapping
After Plaid Link succeeds, Encore OS uses a deferred exchange flow whenonPlaidLinkSuccess is set:
- Link returns
public_tokenandmetadata.accounts(all accounts the user linked at the institution). - The app shows PlaidAccountMappingDialog: user selects which accounts to connect and assigns a GL account to each.
- Only selected accounts are sent to
plaid-exchange-tokenwithgl_account_idper account. - Backend creates
fa_bank_accountsrows and triggers initial sync for each.
plaid-exchange-token accepts accounts[] with optional gl_account_id. If provided, that GL is used (and must be an asset, posting-allowed account in the same organization); otherwise the server falls back to default cash/asset lookup.
Plaid docs: metadata.accounts in the Link onSuccess callback contains id, name, mask, type, subtype. Account selection in Link is controlled by the user inside Plaid’s UI; we do not filter which accounts appear in Link for initial link (optional: use account_filters when creating the link token to limit types/subtypes).
Recommendations (from Plaid docs and product)
Product initialization
We request Transactions and Balance when creating Link tokens. The Balance product is required for/accounts/balance/get and for populating fa_bank_accounts.balance; without it, Bank Summary and Cash Allocation widgets will show zero or omit the account. Existing Items created with only transactions do not have Balance—re-linking (or Plaid update mode with balance added) is needed to get balance. When we add Auth or Identity, consider optional_products (Auth) or required_if_supported_products (Identity) per Plaid’s Choosing when to initialize products.
Balance and cash position
When Plaid updatesfa_bank_accounts.balance (in plaid-sync after a manual or post-exchange sync, or in plaid-webhook on SYNC_UPDATES_AVAILABLE), the same cash position flow as Teller is used: we invoke the fa-handle-bank-balance-updated edge function with organization_id, bank_account_id, old_balance, new_balance, and optional updated_by. That handler recalculates from all active bank accounts and upserts fa_cash_positions for the current date, so the Cash Allocation & Credit widget stays in sync with Plaid balance changes.
References
- Spec:
specs/fa/specs/FA-PLAID-INTEGRATION-SPEC.md - Review:
specs/fa/reviews/FA-PLAID-INTEGRATION-REVIEW.md - Teller Integration (retired):
docs/architecture/integrations/FA_TELLER_INTEGRATION.md - Plaid Sandbox verification:
docs/testing/PLAID_SANDBOX_VERIFICATION.md(env, payroll $11.11, Transfer UI flow, optional script) - Plaid Docs: https://plaid.com/docs/
- Plaid API Reference: https://plaid.com/docs/api/
- react-plaid-link: https://github.com/plaid/react-plaid-link