Connect FlutterFlow to Salesforce by building a Cloud Function that handles OAuth 2.0 authentication and proxies API calls to Salesforce REST API v58.0. The Cloud Function manages token refresh every 2 hours. Use endpoints GET /sobjects/Contact for contacts, POST /sobjects/Lead to create leads, and PATCH /sobjects/Opportunity/{id} to update deals. Mirror data to Firestore for offline access.
Build a mobile Salesforce CRM companion app in FlutterFlow
Sales reps need quick mobile access to their Salesforce data — viewing contacts before a meeting, logging a call note immediately after, or updating a deal stage on the drive back to the office. FlutterFlow can power this use case, but Salesforce's OAuth tokens expire every 2 hours and the client_secret must never appear in mobile app code. The solution is a Cloud Function that handles all Salesforce authentication server-side and exposes clean REST endpoints for FlutterFlow to call. This tutorial walks through building the Cloud Function proxy, querying and writing Salesforce objects, syncing data to Firestore for offline access, and receiving Salesforce Outbound Messages when records change.
Prerequisites
- A Salesforce org (Developer Edition is free at developer.salesforce.com) with at least a few Contact and Opportunity records
- A Connected App created in Salesforce: Setup → App Manager → New Connected App with OAuth enabled and callback URL set to https://login.salesforce.com/services/oauth2/success
- Firebase project with Cloud Functions enabled (requires Blaze pay-as-you-go billing)
- Node.js installed locally for writing and deploying Cloud Functions
Step-by-step guide
Create the Salesforce Connected App and get OAuth credentials
Create the Salesforce Connected App and get OAuth credentials
In your Salesforce org, go to Setup (gear icon top-right) → App Manager → New Connected App. Fill in: Connected App Name (e.g., FlutterFlowMobile), Contact Email (your email), Enable OAuth Settings (check the box), Callback URL (https://login.salesforce.com/services/oauth2/success). Under Selected OAuth Scopes, add: Access and manage your data (api), Perform requests at any time (refresh_token, offline_access). Save. Salesforce takes 2-10 minutes to activate the app. After activation, click Manage → View Consumer Key and Consumer Secret — copy both. These are your client_id and client_secret for OAuth. In your Firebase project, go to Cloud Functions → open your functions/index.js file (or create it) and store these as environment config: run firebase functions:config:set salesforce.client_id='YOUR_ID' salesforce.client_secret='YOUR_SECRET' salesforce.username='your@email.com' salesforce.password='yourpassword+securitytoken' from your terminal. The security token is found in Salesforce: Settings (top-right avatar) → My Personal Information → Reset My Security Token.
Expected result: Connected App is active in Salesforce and OAuth credentials are stored securely in Firebase Functions config — not in FlutterFlow or app code.
Build the Cloud Function proxy that handles Salesforce OAuth
Build the Cloud Function proxy that handles Salesforce OAuth
Create a Cloud Function named salesforce that authenticates with Salesforce's username-password OAuth flow and proxies requests. The function accepts a POST request from FlutterFlow with a JSON body specifying the Salesforce operation: {action: 'getContacts'}, {action: 'createLead', data: {...}}, {action: 'updateOpportunity', id: '...', data: {...}}. The function first calls Salesforce's token endpoint to get a fresh access_token and instance_url, then uses those to make the actual API call, then returns the result to FlutterFlow. Cache the access token in Firestore (with the expiry time) and reuse it until 5 minutes before expiry to avoid a new OAuth call on every request.
1// functions/index.js — Salesforce Cloud Function proxy2const functions = require('firebase-functions');3const admin = require('firebase-admin');4const axios = require('axios');56admin.initializeApp();78const SF_CONFIG = {9 loginUrl: 'https://login.salesforce.com',10 apiVersion: 'v58.0',11};1213// Get or refresh Salesforce access token14async function getSalesforceToken() {15 const db = admin.firestore();16 const tokenDoc = await db.collection('_sf_tokens').doc('current').get();17 const now = Date.now();1819 // Reuse cached token if valid for 5+ more minutes20 if (tokenDoc.exists) {21 const { accessToken, instanceUrl, expiresAt } = tokenDoc.data();22 if (expiresAt > now + 300000) {23 return { accessToken, instanceUrl };24 }25 }2627 // Request new token via username-password flow28 const cfg = functions.config().salesforce;29 const params = new URLSearchParams({30 grant_type: 'password',31 client_id: cfg.client_id,32 client_secret: cfg.client_secret,33 username: cfg.username,34 password: cfg.password,35 });36 const resp = await axios.post(37 `${SF_CONFIG.loginUrl}/services/oauth2/token`,38 params.toString(),39 { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }40 );4142 const { access_token, instance_url } = resp.data;43 // Cache token — Salesforce tokens valid for ~2 hours44 await db.collection('_sf_tokens').doc('current').set({45 accessToken: access_token,46 instanceUrl: instance_url,47 expiresAt: now + 7200000, // 2 hours48 });49 return { accessToken: access_token, instanceUrl: instance_url };50}5152exports.salesforce = functions.https.onRequest(async (req, res) => {53 res.set('Access-Control-Allow-Origin', '*');54 if (req.method === 'OPTIONS') { res.status(204).send(''); return; }5556 try {57 const { action, id, data, query } = req.body;58 const { accessToken, instanceUrl } = await getSalesforceToken();59 const base = `${instanceUrl}/services/data/${SF_CONFIG.apiVersion}`;60 const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' };6162 let result;63 if (action === 'getContacts') {64 const soql = `SELECT Id,Name,Email,Phone,Account.Name FROM Contact ORDER BY LastModifiedDate DESC LIMIT 50`;65 const r = await axios.get(`${base}/query?q=${encodeURIComponent(soql)}`, { headers });66 result = r.data.records;67 } else if (action === 'createLead') {68 const r = await axios.post(`${base}/sobjects/Lead`, data, { headers });69 result = r.data;70 } else if (action === 'updateOpportunity') {71 await axios.patch(`${base}/sobjects/Opportunity/${id}`, data, { headers });72 result = { success: true };73 } else if (action === 'getOpportunities') {74 const soql = `SELECT Id,Name,StageName,Amount,CloseDate,AccountId FROM Opportunity ORDER BY LastModifiedDate DESC LIMIT 50`;75 const r = await axios.get(`${base}/query?q=${encodeURIComponent(soql)}`, { headers });76 result = r.data.records;77 }78 res.json({ success: true, data: result });79 } catch (err) {80 console.error('Salesforce CF error:', err.response?.data || err.message);81 res.status(500).json({ success: false, error: err.message });82 }83});Expected result: Cloud Function is deployed and returns Salesforce contact data when called with {action: 'getContacts'} from Postman or curl.
Configure the FlutterFlow API call to your Cloud Function
Configure the FlutterFlow API call to your Cloud Function
In FlutterFlow, go to API Manager → Add API Group. Name it SalesforceProxy. Set the Base URL to your deployed Cloud Function URL — it looks like https://us-central1-{your-project}.cloudfunctions.net. Set the Method to POST. Under Headers, add Content-Type: application/json. Add an individual API Call named getContacts. Under Request Body, add a JSON body: {"action": "getContacts"}. Click Test — the response should show your Salesforce contacts as a JSON array. Similarly add createLead with body {"action": "createLead", "data": {"FirstName": "[firstName]", "LastName": "[lastName]", "Company": "[company]", "Email": "[email]"}} using FlutterFlow's variable syntax [variableName] for the dynamic fields. Add updateOpportunity with body {"action": "updateOpportunity", "id": "[opportunityId]", "data": {"StageName": "[stageName]"}}. Extract response JSON paths: for getContacts, FlutterFlow will parse the data array — mark the fields you need (Id, Name, Email, Phone) in the response inspector.
Expected result: SalesforceProxy API Group shows three API Calls, each testing successfully and returning the expected Salesforce data in the response inspector.
Build the contacts ListView and lead creation form
Build the contacts ListView and lead creation form
Create a Contacts page in FlutterFlow. Add a Backend Query to the page using the getContacts API call. Add a ListView widget — in its properties, click Generate Dynamic Children and bind to the query result's data array. Each generated child is a ContactCard component: add a Column widget containing a Text widget bound to item.Name, a Row with an email icon + Text bound to item.Email, a phone icon + Text bound to item.Phone, and a Text bound to item.Account.Name in grey. Add an onTap action to the Column that navigates to a ContactDetail page passing item.Id and item.Name as page parameters. For lead creation: add a floating action button on the Contacts page. On Tap: Show Bottom Sheet containing a form with TextFields for firstName, lastName, company, and email, plus a Submit Button. Submit Button Action Flow: call createLead API with the TextField values, show a SnackBar 'Lead created in Salesforce', dismiss the bottom sheet, and refresh the Backend Query on the parent page.
Expected result: Contacts page shows a list of Salesforce contacts with names, emails, and company names. The FAB opens a lead creation form that creates a real Salesforce Lead record.
Sync Salesforce data to Firestore for offline access
Sync Salesforce data to Firestore for offline access
Mobile apps need offline access — if the user opens the app on the subway with no signal, they should still see their contact list from the last sync. Add a syncSalesforceData Cloud Function that fetches contacts and opportunities from Salesforce and writes them to Firestore collections sf_contacts and sf_opportunities. Schedule it with Cloud Scheduler to run every 15 minutes using a Pub/Sub trigger. In FlutterFlow, change the Backend Query on the Contacts page to use Firestore (read from sf_contacts collection) instead of the direct API call — this gives instant display from Firestore cache. Add a pull-to-refresh gesture on the ListView: On Refresh → call the SalesforceProxy getContacts API → write each contact to Firestore sf_contacts/{id} document. The Cloud Scheduler ensures data stays current even when the user does not manually refresh.
Expected result: Contacts page loads instantly from Firestore. Pull-to-refresh updates Firestore from live Salesforce data. Data is accessible offline from the last sync.
Complete working example
1Cloud Function: salesforce (HTTPS onRequest)2═══════════════════════════════════════════3Actions handled:4 getContacts → SOQL: SELECT Id,Name,Email,Phone,Account.Name5 FROM Contact ORDER BY LastModifiedDate DESC LIMIT 506 getOpportunities → SOQL: SELECT Id,Name,StageName,Amount,CloseDate7 FROM Opportunity ORDER BY LastModifiedDate DESC LIMIT 508 createLead → POST /sobjects/Lead9 { FirstName, LastName, Company, Email, Phone }10 updateOpportunity → PATCH /sobjects/Opportunity/{id}11 { StageName: 'Closed Won' | 'Closed Lost' | etc. }1213OAuth: username-password flow → cached in Firestore _sf_tokens/current14Token refresh: auto-refresh when token expires within 5 minutes1516Firestore Sync Collections17══════════════════════════18sf_contacts/{salesforceId}19 name, email, phone, accountName, lastSyncedAt2021sf_opportunities/{salesforceId}22 name, stageName, amount, closeDate, accountId, lastSyncedAt2324sf_metadata/sync25 contactsLastSync: Timestamp26 opportunitiesLastSync: Timestamp2728FlutterFlow Pages29══════════════════30Contacts31 Backend Query: sf_contacts (Firestore, real-time)32 ListView → ContactCard (name, email, phone, company)33 FAB → Create Lead Bottom Sheet34 Fields: firstName, lastName, company, email35 Submit → createLead API → SnackBar → dismiss3637ContactDetail (param: contactId, contactName)38 Backend Query: sf_contacts/{contactId} (Firestore)39 Display: full contact info + recent activities40 Button: Create Follow-up → creates Salesforce Task4142Opportunities43 Backend Query: sf_opportunities (Firestore)44 ListView → OpportunityCard (name, stage, amount, closeDate)45 DropDown (StageName) → On Change → updateOpportunity API4647API Manager: SalesforceProxy48══════════════════════════════49Base URL: https://us-central1-{project}.cloudfunctions.net50Method: POST51Header: Content-Type: application/json5253Calls:54 getContacts { "action": "getContacts" }55 createLead { "action": "createLead", "data": {...} }56 updateOpportunity { "action": "updateOpportunity", "id": "[id]", "data": {...} }Common mistakes
Why it's a problem: Making Salesforce API calls directly from FlutterFlow's API Manager without a Cloud Function, putting the client_secret in the API Group headers
How to avoid: All Salesforce API calls must go through a Cloud Function. The Cloud Function stores client_id, client_secret, username, and password in Firebase Functions config (server-side secrets), handles token refresh transparently, and exposes a clean action-based REST API for FlutterFlow to call.
Why it's a problem: Querying Salesforce directly on every page load instead of reading from the Firestore sync cache
How to avoid: Sync Salesforce data to Firestore on a schedule (every 15 minutes via Cloud Scheduler). Read from Firestore in FlutterFlow for instant display. Only call the Salesforce API directly when the user explicitly triggers a write operation (creating a lead, updating a stage) or manually refreshes.
Why it's a problem: Using the deprecated Salesforce API key (Simple_API_Key) instead of the Connected App OAuth credentials
How to avoid: Create a Connected App in Setup → App Manager → New Connected App with OAuth enabled. Use the Consumer Key as client_id and Consumer Secret as client_secret in the OAuth token request. The username-password OAuth flow works without user interaction, suitable for server-to-server integration.
Best practices
- Always proxy Salesforce API calls through a Cloud Function — never put OAuth credentials or access tokens in FlutterFlow's API Manager headers
- Cache Salesforce access tokens in Firestore with their expiry time and reuse them across requests to minimize OAuth round-trips
- Sync read-heavy data (contacts, opportunities) to Firestore on a schedule so the app loads instantly and works offline
- Use SOQL LIMIT clauses in all queries — SELECT * without LIMIT can return thousands of records and hit Salesforce's batch size limit of 2,000 records per query
- Set up Salesforce Outbound Messages for records that change frequently (opportunity stage changes, contact updates) so your Firestore cache stays current without polling
- Log every Salesforce write operation (createLead, updateOpportunity) to a Firestore audit_log collection with userId, timestamp, action, and recordId for accountability
- Test with Salesforce Developer Edition (free sandbox) before connecting to a production org — mistakes in production Salesforce can affect live CRM data
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a FlutterFlow app that needs to integrate with Salesforce CRM. Write a Firebase Cloud Function in Node.js that: (1) authenticates with Salesforce using the OAuth 2.0 username-password flow to get an access token, (2) caches the access token with its expiry time and refreshes it automatically, (3) handles these actions via POST request: getContacts (SOQL query for Id, Name, Email, Phone, Account.Name), createLead (POST to sobjects/Lead), updateOpportunity (PATCH to sobjects/Opportunity). Include error handling and CORS headers.
Create a page called Contacts with a Backend Query that reads from the sf_contacts Firestore collection. Add a ListView that displays each contact's name, email, and company in a card. Add a floating action button that opens a bottom sheet form with fields for first name, last name, company name, and email, with a Submit button that calls the SalesforceProxy createLead API call.
Frequently asked questions
Do I need a paid Salesforce license to use the REST API?
Salesforce Developer Edition is free and includes full REST API access — it is perfect for development and testing. For production apps used by your sales team, each user needs at least a Salesforce Professional Edition license ($75/user/month). The API call limits vary by edition: Essentials (no API access), Professional (1,000 API calls/day per license), Enterprise (15,000 API calls/day per org + 1,000 per license). Developer Edition has 15,000 API calls/day.
Can FlutterFlow users log in with their own Salesforce credentials?
Yes, using the Salesforce OAuth 2.0 web server flow instead of the username-password flow. The user taps 'Log in with Salesforce' → your Cloud Function redirects them to Salesforce's login page → they authenticate → Salesforce redirects back with an auth code → Cloud Function exchanges the code for access + refresh tokens specific to that user. Each user's access token is scoped to their own Salesforce data and permissions. This is more complex than the single service-account approach covered in this tutorial.
How do I handle Salesforce sandbox vs production environments?
Salesforce sandbox uses login.salesforce.com for production and test.salesforce.com for sandbox. In your Cloud Function, store the login URL as an environment variable: firebase functions:config:set salesforce.login_url='https://test.salesforce.com' for sandbox builds and 'https://login.salesforce.com' for production. Use separate Firebase projects (and thus separate Cloud Functions) for your development and production environments.
What Salesforce objects can I access via the REST API?
All standard Salesforce objects: Account, Contact, Lead, Opportunity, Case, Task, Event, User, Campaign, Contract, and hundreds more. Also any custom objects your org has created — they appear as CustomObjectName__c in the API. Use GET /services/data/v58.0/sobjects/ to get a list of all objects your Connected App has permission to access. Salesforce's REST API covers the full CRM data model.
How do I handle Salesforce SOQL queries with complex filters?
Build the SOQL query string in your Cloud Function using JavaScript template literals. SOQL syntax: SELECT Fields FROM ObjectName WHERE Condition ORDER BY Field LIMIT N. For example: SELECT Id,Name FROM Contact WHERE Email LIKE '%@apple.com' AND LastModifiedDate > 2024-01-01T00:00:00Z ORDER BY Name ASC LIMIT 100. URL-encode the query with encodeURIComponent() before appending to the /query? endpoint. For parameterized queries, build the string with variables: const soql = SELECT ... WHERE OwnerId = '${userId}' — this is safe inside the Cloud Function since the userId comes from authenticated Firebase calls.
Can RapidDev build a complete Salesforce mobile CRM companion app in FlutterFlow?
Yes. A production Salesforce mobile companion with user login via Salesforce OAuth, full CRUD on contacts/opportunities/tasks/cases, offline sync with conflict resolution, push notifications on record assignments, and custom Salesforce object support goes well beyond this basic integration. RapidDev has built Salesforce-connected FlutterFlow apps for field sales teams and can architect a solution that stays within your org's API limits.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation