Skip to content

[FEATURE] Add Bridge BYOK provider support#2

Open
falcononrails wants to merge 2 commits intocdinnison:mainfrom
falcononrails:feature/bridge-byok
Open

[FEATURE] Add Bridge BYOK provider support#2
falcononrails wants to merge 2 commits intocdinnison:mainfrom
falcononrails:feature/bridge-byok

Conversation

@falcononrails
Copy link
Copy Markdown

@falcononrails falcononrails commented Apr 13, 2026

Summary

  • add Bridge as a second BYOK banking provider alongside Plaid
  • introduce a small provider layer so link + sync dispatch by institution provider
  • keep managed mode Plaid-only and preserve existing Plaid behavior

What changed

  • extend ray setup config to store optional Bridge BYOK credentials and a reusable default Bridge external_user_id
  • make ray link provider-aware, with Bridge support for:
    • auto-create/reuse of a Ray-managed Bridge user
    • linking with an existing external_user_id
    • reconnect/update flows for Bridge items that require user action
  • add provider-aware institution state via provider, provider_user_id, and provider_state
  • add Bridge account + transaction sync into the existing generic accounts and transactions tables
  • surface Bridge reconnect-needed state in ray accounts and ray doctor

Notes

  • Bridge is BYOK-only in this pass
  • Bridge Connect completion is detected by polling rather than a localhost callback
  • this scope intentionally excludes Bridge holdings/stocks, liabilities enrichment, recurring detection, and webhooks
  • the existing src/plaid/* implementation is intentionally left in place for this PR; the provider boundary is added here, and moving Plaid fully under src/providers/ should be done in a follow-up refactor to keep this diff reviewable and lower-risk

Discussion

Validation

  • npm run build
  • npm test

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 13, 2026

@falcononrails is attempting to deploy a commit to the Otherness Team on Vercel.

A member of the Team first needs to authorize it.

@falcononrails falcononrails changed the title Add Bridge BYOK provider support [FEATURE] Add Bridge BYOK provider support Apr 13, 2026
@falcononrails falcononrails force-pushed the feature/bridge-byok branch 3 times, most recently from d116da7 to e3000a0 Compare April 13, 2026 15:38
Copy link
Copy Markdown
Owner

@cdinnison cdinnison left a comment

Choose a reason for hiding this comment

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

Thanks for this contribution — really well-structured PR overall. The provider abstraction is clean, the Bridge client is well-factored (injectable fetch, pagination, proper error class), and the incremental approach of wrapping Plaid rather than moving it is the right call for keeping this reviewable.

Test coverage is solid — the deleted transaction, incremental sync cursor, and reconnect state tests are thorough. The import-existing-items flow is a nice UX touch.

A few things to address before merging — see inline comments. The main ones are:

  1. Stale provider_state for reconnect messages — the daily-sync reconnect block reads pre-sync state and is redundant with result.message
  2. getItem fallback catches all errors — should scope to 404
  3. formatMoney tests were gutted — need to still verify actual output for default locale
  4. Inline import() types — should use top-level imports
  5. bridgeClientId masked as password — client IDs aren't secret

Also a couple of non-blocking notes on currency formatting scope and the polling timeout.

Comment thread src/daily-sync.ts Outdated
if (result.reconnectRequired && providerKey === "bridge") {
const state = parseProviderState<BridgeProviderState>(inst.provider_state);
const reconnectMessage = describeBridgeProviderState(state);
if (reconnectMessage) console.log(` ${reconnectMessage}`);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This block is redundant — syncBridgeInstitution already sets result.message with the reconnect description when reconnectRequired is true (see index.ts:405). And it has a bug: it reads the pre-sync inst.provider_state rather than the state that was just updated in the DB by syncInstitution.

Suggestion: drop this entire if (result.reconnectRequired && providerKey === "bridge") block. The result.message line above it already handles this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3f06cc9. I removed that redundant reconnect block from daily sync so it now relies on provider sync result messaging only.

Comment thread src/providers/bridge/index.ts Outdated
}

const accessToken = await client.ensureAccessToken(externalUserId);
const item = await client.getItem(accessToken, institution.item_id).catch(async () => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This .catch() swallows all errors — network failures, auth errors, rate-limits — and falls back to listing all items. If getItem fails for a non-404 reason, listItems will likely fail too, just with a more confusing error.

Suggestion: scope the catch to 404 only:

const item = await client.getItem(accessToken, institution.item_id).catch(async (err) => {
  if (!(err instanceof BridgeApiError && err.status === 404)) throw err;
  const items = await client.listItems(accessToken);
  // ...
});

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3f06cc9. The getItem fallback now only catches BridgeApiError 404; all other errors are re-thrown.

Comment thread src/providers/bridge/index.ts Outdated
}

function upsertBridgeInstitution(
db: import("../types.js").Database,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Database and InstitutionRecord are used throughout this file via inline import("../types.js") annotations (~10 occurrences). Since InstitutionProvider is already imported from types.js at the top, just add them to that import:

import type { InstitutionProvider, Database, InstitutionRecord } from "../types.js";

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3f06cc9. I imported Database and InstitutionRecord at the top-level type import and removed inline import() type annotations.

},
): Promise<BridgeItem[]> {
const previousById = new Map(options.previousItems.map(item => [String(item.id), item]));
const deadline = Date.now() + 10 * 60 * 1000;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Non-blocking: 10-minute polling at 3-second intervals with no escape hatch — if the user abandons the Bridge Connect flow, they're stuck waiting. Consider printing a hint like "Press Ctrl+C to cancel" alongside the spinner, or shortening the timeout.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 3f06cc9. Added an explicit "Press Ctrl+C to cancel waiting" hint before polling Bridge Connect completion.

Comment thread src/cli/setup.ts Outdated
},
{
theme,
type: "password",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Client IDs aren't secret — this should use type: "input" instead of type: "password" so the user can see what they're typing. Keep bridgeClientSecret as password.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3f06cc9. bridgeClientId prompt now uses type: input (bridgeClientSecret remains password).

Comment thread src/queries/index.test.ts Outdated
it("formats negative as absolute", () => expect(formatMoney(-99)).toBe("$99.00"));
it("formats zero", () => expect(formatMoney(0)).toBe("$0.00"));
it("formats positive", () => {
expect(formatMoney(1234.5)).toMatch(/\d/);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

These assertions are too weak now — toMatch(/\d/) and toContain("1") pass for almost any string containing a number. The currency formatting change is fine, but the tests should still verify the default output.

Suggestion: set config.displayLocale = "en-US" and config.displayCurrency = "USD" in the test (with afterEach cleanup) and assert toBe("$1,234.50"). That way you're testing the formatting logic, not just that a digit exists.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3f06cc9. I strengthened formatMoney tests by forcing en-US/USD in test setup and asserting exact outputs ($1,234.50, $99.00, $0.00), with cleanup after each test.

Comment thread src/currency.ts
@@ -0,0 +1,109 @@
import { config } from "./config.js";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Non-blocking: this module is a nice addition, but it changes output for all users (not just Bridge), and the displayLocale/displayCurrency config options aren't exposed in ray setup. Users can only set them via env vars or editing the config file directly. Consider either adding them as optional fields in setup, or at least mentioning the env vars (RAY_DISPLAY_LOCALE, RAY_DISPLAY_CURRENCY) in ray doctor or help output so they're discoverable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 3f06cc9. I added display format discoverability in ray doctor, including explicit mention of RAY_DISPLAY_LOCALE and RAY_DISPLAY_CURRENCY.

@falcononrails
Copy link
Copy Markdown
Author

falcononrails commented Apr 17, 2026

@cdinnison I pushed follow-up commit 3f06cc9 addressing all inline review items.

  • removed redundant Bridge reconnect message block in daily sync
  • narrowed getItem fallback to BridgeApiError 404 only
  • replaced inline import() type annotations with top-level type imports
  • added Ctrl+C cancel hint while polling Bridge Connect
  • switched bridgeClientId setup prompt to input
  • strengthened formatMoney tests with deterministic en-US/USD assertions
  • added display locale/currency env var discoverability in ray doctor

Verified with npm run build and npm test.

@cdinnison
Copy link
Copy Markdown
Owner

Thank you! I've been out sick so will review ASAP. appreciate the follow-ups

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.

2 participants