Sync Salesforce to Postgres bidirectionally in under 30 minutes
From OAuth to a live bidirectional flow in under half an hour, with the Salesforce-specific gotchas (governor limits, trigger recursion, SOQL caveats) called out where they bite.
- Author
- Ruben Burdin · Founder & CEO
- Published
- June 3, 2026
- Read time
- 15 min
Step 1 — Connect both sides
Salesforce side: connect via OAuth as a dedicated integration user with the minimum profile that grants API access and read/write on the objects you intend to sync. Don't reuse a human user. The integration user should have no UI session, a high API call limit, and a single permission set whose contents are version-controlled.
Postgres side: a role with SELECT, INSERT, UPDATE, and DELETE on the target schema, plus USAGE on the schema itself. Run on SSL. If your Postgres is behind a VPC, prefer reverse SSH tunneling or peering over IP allowlist.
Step 2 — Scope the objects
Pick the smallest set of objects that delivers value. Resist the urge to sync "everything". A useful Phase 1 is usually: Account, Contact, Opportunity, one or two custom objects, and the relationship junction tables you actually query in Postgres. Everything else can be added once the pipeline is observable.
- Filter records at the source using SOQL where-clauses on the connection, not in post-processing.
- If a record has 200+ custom fields, opt in to only the ones you'll join on or report from.
- Set up two flows: a one-time backfill via Bulk API 2.0, and the live stream via Platform Events.
Step 3 — Map and validate
Field-level mapping is where the integration becomes durable. Map Id to a uniqueness-constrained text column, not a generated bigint. Map booleans explicitly; Salesforce's checkbox never returns null, so a Postgres boolean NOT NULL DEFAULT false is correct.
For validation, run the backfill in shadow mode for an hour: writes happen but trigger no downstream consumers. Compare row counts and a sampled checksum across both sides. Only enable the live event stream once the shadow run is clean.
"We expected the SOQL queries to bite first; in practice, governor limits never even registered. The problem we did hit was a custom trigger writing back into a synced field. That's where the trigger storm came from."
FAQ