Skip to main content
RapidDev - Software Development Agency
lovable-integrationsEdge Function Integration

How to Integrate Lovable with Plaid

Integrating Plaid with Lovable lets you add bank account linking and financial data access to your app using Edge Functions as a secure backend proxy. Store your Plaid Client ID and Secret in Cloud Secrets, create Edge Functions to generate Link tokens and exchange public tokens for access tokens, then embed Plaid Link in your React frontend. Setup takes 30–60 minutes in Sandbox, with a separate Plaid application process required for Production access.

What you'll learn

  • How to store Plaid credentials securely in Lovable Cloud Secrets
  • How to create a Plaid Link token using a Deno Edge Function
  • How to exchange the public token for a permanent access token after bank connection
  • How to fetch account balances and transaction data via Edge Functions
  • How to store Plaid access tokens in Supabase with RLS policies protecting user financial data
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read45 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

Integrating Plaid with Lovable lets you add bank account linking and financial data access to your app using Edge Functions as a secure backend proxy. Store your Plaid Client ID and Secret in Cloud Secrets, create Edge Functions to generate Link tokens and exchange public tokens for access tokens, then embed Plaid Link in your React frontend. Setup takes 30–60 minutes in Sandbox, with a separate Plaid application process required for Production access.

Why integrate Plaid with Lovable?

Plaid is the infrastructure layer powering most US fintech apps — it's what Venmo, Robinhood, and thousands of other financial applications use to verify bank accounts and pull transaction data. If you're building anything involving personal finance tracking, expense management, lending qualification, income verification, or ACH payment initiation, Plaid provides the bank connectivity layer that would otherwise take years to build through direct bank partnerships.

Unlike PayPal or Stripe where users enter card details, Plaid's model is different: users authenticate directly with their bank through Plaid Link, a hosted OAuth-style flow that never exposes login credentials to your app. Plaid returns an access token that your backend uses to query that user's financial data going forward. This token architecture is why all Plaid API calls must happen server-side through Edge Functions — access tokens are long-lived credentials that grant real-time financial access and must never appear in client-side code.

Plaid supports three environments: Sandbox (fake data, free, for development), Development (real data, limited to 100 Items, requires Plaid approval), and Production (real data, unlimited, requires Plaid application and approval). This tutorial focuses on Sandbox setup, which lets you build and test the full integration with simulated bank accounts before applying for production access.

Integration method

Edge Function Integration

Plaid has no native Lovable connector. Integration requires Supabase Edge Functions to handle the server-side Plaid API calls: creating Link tokens, exchanging public tokens for access tokens, and fetching account and transaction data. The Plaid Link widget loads in the browser via the react-plaid-link package, but all authenticated Plaid API requests run through Edge Functions using credentials stored in Cloud Secrets.

Prerequisites

  • A Lovable project with Cloud enabled
  • A Plaid developer account — sign up free at dashboard.plaid.com
  • Your Plaid Client ID and Sandbox Secret from the Plaid Dashboard (Keys section)
  • Familiarity with Lovable's Cloud tab and Edge Functions
  • Basic understanding of the Plaid Link flow (user connects bank → you receive public token → exchange for access token)

Step-by-step guide

1

Get your Plaid API credentials from the Plaid Dashboard

Sign up or log in at dashboard.plaid.com. After completing email verification, you land on the main dashboard. In the left sidebar, click 'Team Settings', then 'Keys'. You'll see three sets of credentials: Client ID (same across all environments), Sandbox Secret, Development Secret, and Production Secret. Copy your Client ID — it's a 24-character alphanumeric string like '5f2e1b3c4d5e6f7a8b9c0d1e'. Then copy your Sandbox Secret. These are the only two credentials you need for development. The Sandbox environment provides simulated bank accounts with test usernames and passwords — no real banking credentials are involved. While in the Plaid Dashboard, also note the available products listed in your account. For basic account linking and balance checks, the 'Auth' and 'Balance' products are sufficient. For transaction history, you need the 'Transactions' product. These product selections affect which data Plaid returns and appear in your Link token creation request — you'll specify them in the Edge Function.

Pro tip: Plaid's Sandbox environment includes test institutions with preset credentials. The most useful test account is 'user_good' with password 'pass_good' at First Platypus Bank — it provides a checking account with transaction history for testing.

Expected result: You have your Plaid Client ID and Sandbox Secret copied from the Plaid Dashboard Keys page.

2

Store Plaid credentials in Lovable Cloud Secrets

Open your Lovable project. Click the '+' button in the top-right area of the editor interface to access the panels, then select 'Cloud'. In the Cloud panel, find and expand the 'Secrets' section. You'll use the Name/Value form to add encrypted environment variables accessible only from your Edge Functions. Add the following secrets one at a time: First, set Name to PLAID_CLIENT_ID and paste your Plaid Client ID as the Value, then click 'Add Secret'. Second, set Name to PLAID_SECRET and paste your Sandbox Secret, then click 'Add Secret'. Third, add PLAID_ENV with value 'sandbox' — this tells your Edge Functions which Plaid API environment to call. Plaid's API base URLs are environment-specific: Sandbox uses https://sandbox.plaid.com, Development uses https://development.plaid.com, and Production uses https://production.plaid.com. Reading PLAID_ENV in your Edge Function lets you switch environments by updating a single secret rather than changing code. Never set PLAID_ENV to 'production' until you have received Plaid's production access approval, which requires a separate application process. These secrets are encrypted at rest and in transit, and are accessible only from server-side Edge Function code through Deno.env.get(). They are completely isolated from your frontend React code.

Pro tip: When you're ready to move to Plaid's Development environment (which uses real banks with limited items), simply update PLAID_SECRET with your Development Secret and change PLAID_ENV to 'development'. No code changes needed.

Expected result: PLAID_CLIENT_ID, PLAID_SECRET, and PLAID_ENV appear in your Cloud Secrets list. None of these values appear anywhere in your project source code.

3

Create the Plaid Link token and token exchange Edge Functions

Plaid's connection flow has two distinct server-side steps. First, your backend creates a Link token — a short-lived, single-use token that the Plaid Link widget uses to initialize the bank connection interface. Second, after the user successfully connects their bank account through the Link UI, the browser receives a public_token that must be exchanged server-side for a permanent access_token. Create a single Edge Function that handles both operations through an action parameter. The Link token creation requires your Client ID, Secret, a user object with a client_user_id, the Plaid products you want access to, the country codes for supported institutions, and the language for the Link UI. The public token exchange requires your Client ID, Secret, and the public_token received from the browser. The access_token returned by the exchange endpoint is the most sensitive piece of data in the entire integration. It grants ongoing read access to a user's bank account and must be stored in your Supabase database with RLS policies that allow only the owning user to read their own token, and only your Edge Functions (using the service role key) to write tokens. Never return the access token to the frontend — store it in your database and reference it by the item_id Plaid also returns.

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/plaid-link/index.ts. It should read PLAID_CLIENT_ID, PLAID_SECRET, and PLAID_ENV from Deno.env to construct the Plaid API base URL. Support two actions via POST body: action='create-link-token' takes a user_id and calls POST /link/token/create with products=['auth','transactions'], country_codes=['US'], language='en', and a client_user_id equal to the user_id; return the link_token. action='exchange-token' takes a public_token and user_id, calls POST /item/public_token/exchange, then saves the returned access_token and item_id to a plaid_connections table in Supabase (columns: user_id, item_id, access_token, institution_name, created_at) using the service role key. Return only the item_id to the frontend, never the access_token.

Paste this in Lovable chat

supabase/functions/plaid-link/index.ts
1// supabase/functions/plaid-link/index.ts
2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
4
5const PLAID_ENV = Deno.env.get("PLAID_ENV") ?? "sandbox";
6const PLAID_BASE = `https://${PLAID_ENV}.plaid.com`;
7const CLIENT_ID = Deno.env.get("PLAID_CLIENT_ID") ?? "";
8const SECRET = Deno.env.get("PLAID_SECRET") ?? "";
9
10const corsHeaders = {
11 "Access-Control-Allow-Origin": "*",
12 "Access-Control-Allow-Methods": "POST, OPTIONS",
13 "Access-Control-Allow-Headers": "Content-Type, Authorization",
14};
15
16serve(async (req) => {
17 if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
18
19 try {
20 const { action, user_id, public_token } = await req.json();
21
22 if (action === "create-link-token") {
23 const res = await fetch(`${PLAID_BASE}/link/token/create`, {
24 method: "POST",
25 headers: { "Content-Type": "application/json" },
26 body: JSON.stringify({
27 client_id: CLIENT_ID,
28 secret: SECRET,
29 user: { client_user_id: user_id },
30 client_name: "My Lovable App",
31 products: ["auth", "transactions"],
32 country_codes: ["US"],
33 language: "en",
34 }),
35 });
36 const data = await res.json();
37 if (data.error_code) throw new Error(data.error_message);
38 return new Response(JSON.stringify({ link_token: data.link_token }), {
39 headers: { ...corsHeaders, "Content-Type": "application/json" },
40 });
41 }
42
43 if (action === "exchange-token") {
44 const res = await fetch(`${PLAID_BASE}/item/public_token/exchange`, {
45 method: "POST",
46 headers: { "Content-Type": "application/json" },
47 body: JSON.stringify({ client_id: CLIENT_ID, secret: SECRET, public_token }),
48 });
49 const data = await res.json();
50 if (data.error_code) throw new Error(data.error_message);
51
52 const supabase = createClient(
53 Deno.env.get("SUPABASE_URL") ?? "",
54 Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
55 );
56 await supabase.from("plaid_connections").upsert({
57 user_id,
58 item_id: data.item_id,
59 access_token: data.access_token,
60 created_at: new Date().toISOString(),
61 }, { onConflict: "user_id" });
62
63 return new Response(JSON.stringify({ item_id: data.item_id }), {
64 headers: { ...corsHeaders, "Content-Type": "application/json" },
65 });
66 }
67
68 return new Response(JSON.stringify({ error: "Unknown action" }), { status: 400, headers: corsHeaders });
69 } catch (err) {
70 return new Response(JSON.stringify({ error: err.message }), {
71 status: 500,
72 headers: { ...corsHeaders, "Content-Type": "application/json" },
73 });
74 }
75});

Pro tip: The plaid_connections table should have an RLS policy that prevents users from reading access_token directly. Store it as a server-only field and only expose item_id and institution_name to the frontend.

Expected result: The plaid-link Edge Function is deployed. Calling it with action='create-link-token' and a user_id returns a link_token string. Calling it with action='exchange-token' saves a row to the plaid_connections table.

4

Embed Plaid Link in your React frontend

Plaid provides an official React component library called react-plaid-link that manages the Link iframe lifecycle. Ask Lovable to add this dependency and create a bank connection component. The component flow is: (1) when the user clicks 'Connect Bank', call the plaid-link Edge Function to get a link_token, (2) pass that token to the usePlaidLink hook, (3) when Plaid calls onSuccess with a public_token, send that token to the Edge Function's exchange-token action, (4) on successful exchange, update the UI to show the connected account. The react-plaid-link package handles everything between those two server calls — opening the Plaid Link modal, displaying the institution search, handling the OAuth redirect for institutions like Chase and Bank of America that use OAuth flows, and returning the public_token on success. You should not need to write any direct communication with Plaid from the frontend beyond what goes through your Edge Functions. For institutions using OAuth (Chase, Bank of America, Wells Fargo), Plaid redirects users to the bank's website and back to your app. This requires configuring a redirect_uri in your Link token creation request that matches a URL registered in your Plaid Dashboard. In Sandbox, this is optional since the test institutions use credential-based flows rather than OAuth.

Lovable Prompt

Add a 'Connect Bank Account' button to the /dashboard page. Install the react-plaid-link package. When clicked: 1) call the plaid-link Edge Function with action='create-link-token' and the current user's ID to get a link_token, 2) open Plaid Link using the usePlaidLink hook, 3) when Plaid calls onSuccess with a public_token and metadata, call the Edge Function with action='exchange-token', the public_token, and user ID, 4) on success store the item_id in the user's profile in Supabase and show a 'Bank connected successfully' message with the institution name from metadata.institution.name.

Paste this in Lovable chat

src/components/PlaidLinkButton.tsx
1// src/components/PlaidLinkButton.tsx
2import { useState, useCallback } from "react";
3import { usePlaidLink } from "react-plaid-link";
4import { Button } from "@/components/ui/button";
5import { supabase } from "@/integrations/supabase/client";
6
7interface PlaidLinkButtonProps {
8 userId: string;
9 onSuccess?: (institutionName: string) => void;
10}
11
12export function PlaidLinkButton({ userId, onSuccess }: PlaidLinkButtonProps) {
13 const [linkToken, setLinkToken] = useState<string | null>(null);
14 const [loading, setLoading] = useState(false);
15
16 const getLinkToken = async () => {
17 setLoading(true);
18 const { data, error } = await supabase.functions.invoke("plaid-link", {
19 body: { action: "create-link-token", user_id: userId },
20 });
21 if (error || !data.link_token) {
22 console.error("Failed to create link token", error);
23 setLoading(false);
24 return;
25 }
26 setLinkToken(data.link_token);
27 setLoading(false);
28 };
29
30 const handleSuccess = useCallback(async (publicToken: string, metadata: any) => {
31 const { data } = await supabase.functions.invoke("plaid-link", {
32 body: { action: "exchange-token", public_token: publicToken, user_id: userId },
33 });
34 if (data?.item_id) {
35 onSuccess?.(metadata.institution?.name ?? "Your bank");
36 }
37 }, [userId, onSuccess]);
38
39 const { open, ready } = usePlaidLink({
40 token: linkToken,
41 onSuccess: handleSuccess,
42 });
43
44 if (linkToken && ready) {
45 return (
46 <Button onClick={() => open()} className="w-full">
47 Connect Bank Account
48 </Button>
49 );
50 }
51
52 return (
53 <Button onClick={getLinkToken} disabled={loading} className="w-full">
54 {loading ? "Loading..." : "Connect Bank Account"}
55 </Button>
56 );
57}

Pro tip: Plaid Link tokens expire after 30 minutes. Don't pre-fetch the link token on page load — fetch it only when the user clicks the Connect button to ensure it's always fresh when Link opens.

Expected result: The 'Connect Bank Account' button appears on the dashboard. Clicking it opens the Plaid Link modal. Using the Sandbox test credentials (user_good / pass_good at First Platypus Bank) completes the connection and shows a success message.

5

Fetch account balances and transactions via Edge Function

With a connected bank account, you can now retrieve financial data. Create Edge Functions for the two most common data retrieval operations: account balance checks and transaction history. Both require the user's stored access_token, which your Edge Function retrieves from the plaid_connections table using the service role key (bypassing RLS to access server-only data). The Balance endpoint (/accounts/balance/get) returns real-time balance data for all accounts linked in an item. It's a synchronous call and returns data immediately. The Transactions endpoint (/transactions/sync) uses a cursor-based pagination system introduced in 2023 that returns added, modified, and removed transactions since the last sync — this is more efficient than the older /transactions/get for keeping data up to date. Store fetched transaction data in your Supabase transactions table and keep a sync cursor per plaid_connection so you only fetch new data on subsequent calls. For complex Plaid architectures involving multiple users with many connected accounts, webhooks from Plaid (for the TRANSACTIONS_SYNC event) are more efficient than polling — RapidDev's team can help architect a webhook-driven sync system that scales to thousands of users.

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/plaid-data/index.ts. It should support two actions: action='get-balances' takes a user_id, fetches the user's access_token from the plaid_connections table using service role key, calls Plaid /accounts/balance/get, and returns the list of accounts with name, type, and available/current balances. action='sync-transactions' takes a user_id, fetches the access_token, calls Plaid /transactions/sync with the stored cursor (or empty string for first sync), saves new transactions to the transactions table in Supabase (columns: user_id, account_id, transaction_id, name, amount, date, category array, merchant_name), and saves the updated cursor back to plaid_connections.

Paste this in Lovable chat

supabase/functions/plaid-data/index.ts
1// supabase/functions/plaid-data/index.ts
2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
4
5const PLAID_ENV = Deno.env.get("PLAID_ENV") ?? "sandbox";
6const PLAID_BASE = `https://${PLAID_ENV}.plaid.com`;
7const CLIENT_ID = Deno.env.get("PLAID_CLIENT_ID") ?? "";
8const SECRET = Deno.env.get("PLAID_SECRET") ?? "";
9const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization" };
10
11serve(async (req) => {
12 if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
13 const supabase = createClient(Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "");
14
15 try {
16 const { action, user_id } = await req.json();
17 const { data: conn } = await supabase.from("plaid_connections").select("access_token, sync_cursor").eq("user_id", user_id).single();
18 if (!conn) throw new Error("No Plaid connection found for user");
19
20 if (action === "get-balances") {
21 const res = await fetch(`${PLAID_BASE}/accounts/balance/get`, {
22 method: "POST",
23 headers: { "Content-Type": "application/json" },
24 body: JSON.stringify({ client_id: CLIENT_ID, secret: SECRET, access_token: conn.access_token }),
25 });
26 const data = await res.json();
27 return new Response(JSON.stringify({ accounts: data.accounts }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
28 }
29
30 if (action === "sync-transactions") {
31 let cursor = conn.sync_cursor ?? "";
32 let added: any[] = [];
33 let hasMore = true;
34 while (hasMore) {
35 const res = await fetch(`${PLAID_BASE}/transactions/sync`, {
36 method: "POST",
37 headers: { "Content-Type": "application/json" },
38 body: JSON.stringify({ client_id: CLIENT_ID, secret: SECRET, access_token: conn.access_token, cursor }),
39 });
40 const data = await res.json();
41 if (data.error_code) throw new Error(data.error_message);
42 added = added.concat(data.added);
43 cursor = data.next_cursor;
44 hasMore = data.has_more;
45 }
46 if (added.length > 0) {
47 await supabase.from("transactions").upsert(
48 added.map((t) => ({ user_id, transaction_id: t.transaction_id, account_id: t.account_id, name: t.name, amount: t.amount, date: t.date, category: t.category, merchant_name: t.merchant_name ?? "" })),
49 { onConflict: "transaction_id" }
50 );
51 }
52 await supabase.from("plaid_connections").update({ sync_cursor: cursor }).eq("user_id", user_id);
53 return new Response(JSON.stringify({ synced: added.length }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
54 }
55
56 return new Response(JSON.stringify({ error: "Unknown action" }), { status: 400, headers: corsHeaders });
57 } catch (err) {
58 return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });
59 }
60});

Pro tip: Plaid's /transactions/sync endpoint may take up to 30 seconds for the first sync on a new connection as Plaid fetches historical data. For subsequent syncs, only new transactions are returned and the call is fast. Consider running the initial sync as a background operation and showing a loading state to users.

Expected result: Calling the plaid-data Edge Function with action='get-balances' returns a list of accounts with current balances. Calling action='sync-transactions' fetches and stores transaction data in Supabase.

Common use cases

Personal finance dashboard with bank transaction sync

A personal finance app lets users connect their bank accounts through Plaid Link and automatically categorizes and displays their transactions in a custom dashboard. The Edge Function fetches the last 30 days of transactions on demand and stores them in Supabase, where the frontend queries and visualizes them with charts showing spending by category.

Lovable Prompt

Add Plaid bank account linking to my finance app. After a user connects their bank, call an Edge Function to fetch the last 30 days of transactions from Plaid and store them in a transactions table in Supabase with columns: user_id, account_id, transaction_id, amount, date, description, category, merchant_name. Display a summary dashboard showing total spending by category this month.

Copy this prompt to try it in Lovable

Income verification for loan or rental applications

A lending or rental platform needs to verify applicant income before approving applications. Plaid's Income product (or basic account/transaction data) provides bank statement data that can be used to verify regular income deposits. The Edge Function analyzes transaction patterns and returns a structured income summary to support application review.

Lovable Prompt

Build an income verification step for my rental application flow. After the applicant connects their bank account via Plaid Link, call an Edge Function to fetch 3 months of transactions, identify recurring income deposits over $1000, and save a verification record to Supabase with fields: applicant_id, verified_monthly_income, verification_date, plaid_item_id. Show the applicant a confirmation screen with their verified income amount.

Copy this prompt to try it in Lovable

Account balance check before ACH payment initiation

A payment app needs to verify sufficient funds before initiating an ACH transfer to prevent failed payments and associated fees. By checking the real-time account balance via Plaid's Balance endpoint before processing a transfer, the app can warn users of insufficient funds and improve payment success rates.

Lovable Prompt

Before processing an ACH payment, add a balance check step using Plaid. The Edge Function should fetch the real-time balance for the user's connected account using their stored Plaid access token, compare it to the payment amount, and either proceed with the transfer or show an 'Insufficient funds' error. Store balance check results in Supabase for audit purposes.

Copy this prompt to try it in Lovable

Troubleshooting

Plaid Link opens but shows 'Invalid client_id or secret' error

Cause: The PLAID_CLIENT_ID or PLAID_SECRET value in Cloud Secrets doesn't match the credentials in your Plaid Dashboard, or you're using a Secret from the wrong environment (e.g., Production secret with PLAID_ENV set to 'sandbox').

Solution: Open the Plaid Dashboard at dashboard.plaid.com → Team Settings → Keys. Verify the Client ID matches PLAID_CLIENT_ID exactly. Confirm PLAID_SECRET matches the Sandbox Secret (not Development or Production). Each environment has a separate secret — they are not interchangeable. Update the secret in Cloud Secrets if there's a mismatch.

Token exchange fails with 'public token has already been consumed'

Cause: Plaid public tokens are single-use and expire after 30 minutes. If the exchange-token Edge Function is called more than once with the same public_token, or if the token expired before the call was made, Plaid rejects it.

Solution: Ensure the frontend only calls the exchange-token action once per successful Plaid Link session. If your component re-renders and calls the function again with a cached public_token, add a ref or state guard to prevent duplicate calls. If the token expired, the user needs to go through the Plaid Link flow again.

typescript
1// Add a ref guard in your frontend to prevent duplicate exchange calls
2const exchanged = useRef(false);
3
4const handleSuccess = useCallback(async (publicToken: string, metadata: any) => {
5 if (exchanged.current) return;
6 exchanged.current = true;
7 // proceed with exchange...
8}, [userId, onSuccess]);

plaid_connections table access denied — Edge Function can't save the access token

Cause: The Edge Function is using the anon key instead of the service role key to write to the plaid_connections table. The anon key is subject to RLS policies, which (correctly) prevent server-side writes using it.

Solution: Ensure your Edge Function uses SUPABASE_SERVICE_ROLE_KEY when creating the Supabase client for database writes. This key bypasses RLS and allows the Edge Function to write the access_token to the table. The service role key is automatically available as Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') in Lovable Edge Functions without adding it to Cloud Secrets manually.

typescript
1// Use service role key for writing sensitive data in Edge Functions
2const supabase = createClient(
3 Deno.env.get("SUPABASE_URL") ?? "",
4 Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "" // NOT the anon key
5);

Transactions sync returns empty results even after successful bank connection

Cause: Plaid Sandbox transactions may take a few seconds to populate after a new connection. Additionally, the /transactions/sync endpoint requires at least one full refresh cycle before returning data for new items. If the sync cursor is missing from the first call, passing an empty string cursor triggers a full historical sync which may not be instant.

Solution: Add a short delay (2-3 seconds) after token exchange before calling sync-transactions for the first time. In the Sandbox environment, you can also call the /sandbox/item/fire_webhook endpoint with the webhook_type TRANSACTIONS and webhook_code SYNC_UPDATES_AVAILABLE to immediately trigger transaction data population for testing.

Best practices

  • Never return the Plaid access_token to the frontend — store it in Supabase with service role key writes and reference it only by item_id on the client side.
  • Apply RLS policies to the plaid_connections table so that even authenticated users can only read their own item_id and institution_name columns, not the access_token column.
  • Use the /transactions/sync cursor-based approach rather than the older /transactions/get endpoint — it's more efficient, returns only changed data, and handles deletions and updates properly.
  • Store the sync cursor in your database after each successful transaction sync so subsequent calls only fetch new data, reducing API calls and improving performance.
  • Use PLAID_ENV in Cloud Secrets to switch between sandbox, development, and production without code changes — this prevents accidentally calling production APIs during development.
  • Handle Plaid webhook events (TRANSACTIONS_SYNC, ITEM_LOGIN_REQUIRED) via a webhook receiver Edge Function to proactively refresh data and alert users when their bank connection needs re-authentication.
  • Test OAuth institution flows (Chase, Bank of America) in Development with real accounts before going to Production — these flows require redirect_uri configuration that Sandbox doesn't fully simulate.

Alternatives

Frequently asked questions

What Plaid environments should I use during development?

Use Sandbox for all development — it provides simulated bank accounts with test credentials and unlimited API calls at no cost. Development gives you access to real bank data but is limited to 100 linked Items and requires a separate Plaid approval. Only apply for Production access when your app is ready to launch, as it requires a review of your use case and compliance documentation.

How do I handle users who need to re-authenticate their bank connection?

Plaid sends an ITEM_LOGIN_REQUIRED webhook when a user's bank credentials become invalid (due to password changes, MFA updates, or bank policy changes). Your webhook receiver Edge Function should update the connection status in Supabase and notify the user. On the frontend, prompt them to go through the Plaid Link update flow, which uses the same link token creation but with the item's access_token to restore the connection without starting from scratch.

Is storing Plaid access tokens in Supabase safe?

Yes, when done correctly. Plaid access tokens should be stored in a table with RLS policies that prevent direct user access to the token value — only your Edge Functions using the service role key should read and use them. The token column can be excluded from user-facing queries entirely. Supabase encrypts data at rest and in transit, and the service role key is never exposed to the frontend.

What's the difference between Plaid Auth, Transactions, and Balance products?

Auth returns the account and routing numbers needed for ACH transfers. Balance returns real-time account balances. Transactions returns historical and ongoing transaction records with category and merchant data. You specify which products to include when creating the Link token, and your Plaid plan must include access to those products. For basic integrations, Transactions covers most personal finance use cases.

How long does it take to get Plaid Production access?

Plaid Production access requires submitting an application describing your use case, your privacy policy, and how you'll use financial data. Review typically takes 3-7 business days. Plaid may request additional documentation for certain use cases like lending or income verification. Start the application process well before your planned launch date.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.