Last updated
Someone has asked for two-way sync between a SharePoint list and Postgres. Underneath, they need a current copy to query all day and a couple of fields written back with someone's approval.
You have a SharePoint list that people edit every day. It might be a supplier register that purchasing keeps current, or a change request list that a Power Apps form writes to. Now somebody wants the same data in PostgreSQL, perhaps an analyst who needs to join it to the ERP export, or a developer building an internal app on Supabase. The request arrives as “can we have two-way sync between SharePoint and Postgres?” and a search for that phrase turns up vendors with very different answers.
Ask what the two-way part is for and the answer is usually lopsided. The report or the app has to read every row, every few minutes, with the lookups resolved. The writes are a status column and perhaps an approval date, and the people who own the list want a say before either one changes.
What two-way sync with SharePoint means in practice
It helps to split the job in two, because the reads and the writes have very different shapes. The reads are bulk and constant. A Power BI report wants the whole list joined to two lookup lists. An internal app wants to filter by a person column on a list that passed the 5,000-item threshold last year, and it wants the answer without paging through Microsoft Graph. More and more often there is an AI agent as well, answering questions with SQL over the same rows.
The writes are small and they need rules. Someone approves a request in the app and the Status column in SharePoint should say Approved. A scheduled job marks overdue items. Each write touches one item and one or two columns, and the list owner will ask who made it and when. A database that can rewrite every column of every item is rarely what anyone asked for. SPSync does the read half today. The write half is designed and has not shipped.
Reading SharePoint from PostgreSQL with SPSync
On the read side, SPSync signs in to your tenant with read-only Graph permissions and copies the lists you
pick into PostgreSQL tables. Each list becomes a table keyed on the SharePoint item ID. A
lookup column becomes a key column with its display value beside it, so you join on it
the way you would join on any foreign key. Multi-choice and multi-person columns become
child tables, and people resolve into a shared sp_users table. The SharePoint to
PostgreSQL page has the full column mapping and the setup
steps.
After the first load, SPSync reads the Graph delta feed on a schedule (five minutes by
default) and applies only the items that changed. A deleted item keeps its row with
sp_deleted_at set, so last month’s report still adds up. A full reconcile compares the
copy with SharePoint, weekly by default and configurable, and it is how you check the copy
for an audit or after a network outage.
Because the copy is plain Postgres, the usual Postgres tools work on it. You can index whichever columns your app filters on and add views for the reports, and none of it changes the list. A reporting role can get SELECT on just the two tables it needs. Before an AI agent queries the data, you can put row-level security on the tables and point any Postgres MCP server at the replica under a role that only sees what it should. None of those queries touch SharePoint, so they cost nothing against Graph throttling and the list view threshold never comes into it.
Writing changes back to SharePoint, as designed
As of today SPSync does not write to SharePoint. The product contains no Graph write call, which is why the permissions it asks for are read-only. Write-back is designed in detail and has no release date. The design follows the lopsided shape above, with small, approved writes on lists you choose.
It starts with a per-list opt-in. You mark one list as writable and every other table
stays a read-only copy. On a writable table, database triggers set a dirty flag on any row
you change and record a deleted row as a tombstone. Each interval, the writer collects
the dirty rows and sends each item to Graph as a PATCH request with an If-Match header
carrying the row’s sp_etag. That column is already on every replicated row today.
If someone changed the item in SharePoint since the last sync, the etag no longer matches and the design treats the write as a conflict. SharePoint wins by default, with a per-list option to let the database win instead. The losing values go into a conflicts table with both versions, their timestamps and the editor’s name, so a person decides what happens to them. A list can also run in dry-run mode, where the writer logs what it would send and sends nothing, and a cap limits how many items a single run may write. The design also logs every write with its before and after values.
The design covers text, number, currency, yes/no, date, choice, single lookup, person and hyperlink columns. Calculated and system columns stay read-only, and attachments and library files are outside it. Until write-back ships, an app that has to change SharePoint calls Graph itself or hands the change to a Power Automate flow, and SPSync picks up the new value on its next run. The two-way sync roadmap page carries the current state.
SharePoint and PostgreSQL sync tools compared
These are the routes people take for this pairing. The competitor columns describe what each vendor’s own page said when we checked on 20 October 2026.
| SPSync | Layer2 Cloud Connector | Skyvia | Stacksync | Power Automate flow | |
|---|---|---|---|---|---|
| Direction today | SharePoint to Postgres | Both ways, per connection | Both ways | “Direction requires confirmation”, in both directions | Whichever way you build each flow |
| SharePoint connector documented | Yes | Yes, with a PostgreSQL walkthrough | Yes, a SharePoint Lists to PostgreSQL page | No, see the quote below | Yes, Microsoft’s SharePoint connector |
| Write-back to SharePoint | Designed, with no release date | Yes | Yes | Their connector page says it is not established | Yes, one flow action per write |
| When both sides change | Does not arise while read-only; SharePoint wins in the design | A conflict mode you choose, such as PostgreSQL always wins | Not described on the page | Not described on the page | Whatever logic the flow contains |
| Lookups and multi-value columns | Key columns and child tables | Column mapping you configure | Mapping you set up in Skyvia’s designer | Not described on the page | Expressions in each flow |
| Where it runs | Your machine or your own Azure subscription | A Windows service you host | Skyvia’s cloud | Stacksync’s cloud | Microsoft’s cloud |
Stacksync’s PostgreSQL and SharePoint page (stacksync.com/integrations/postgresql-and-sharepoint, checked 20 October 2026) states its own position in one line. It reads “Stacksync support for SharePoint is not established by the connector documentation reviewed for this page.” The page suggests starting with one record and a conversation with their engineers.
Layer2’s guide says a connection can be scheduled every 10 minutes or more often, and Skyvia’s page says its integrations can run as often as once per minute. Both of them write to SharePoint today. The Layer2 comparison, the Skyvia comparison and the Power Automate comparison go into each one in more depth, and the Stacksync comparison covers Stacksync.
When another tool is the better pick
If people edit the same columns on both sides all day, you need two-way sync with conflict rules now, and Layer2 Cloud Connector is built for that. It also covers SharePoint on-premises. Skyvia is a reasonable choice when you want a hosted tool and your record volumes fit its plans. For one list and a few writes a day, such as setting a status when a form is approved, a Power Automate flow is often enough.
SPSync is the better fit when most of the load is reading. That usually means a report or an app on a large list with lookups, where you want tables a developer would have designed by hand. Writes then go through a flow or the app’s own Graph call, and SPSync brings the result back into Postgres within a few minutes.
Desktop, Azure or command line
All three editions run the same engine, write the same tables and use the same licence key, so the choice comes down to who looks after the sync.
Desktop app
For macOS and Windows. Sign in with Microsoft, tick the lists, choose a database and sync from your own machine on the schedule you set. Delegated sign-in needs no admin unless your tenant requires admin consent for apps.
In your Azure
SPSync in your Azure, a managed application for your own subscription, is deployed with you on a call while the Marketplace listing is prepared. Once deployed it is always on, so the sync keeps running with every laptop closed.
Command line
Every replication is a YAML file you can review and commit. Run it from a server, a container or a CI pipeline, and re-run it safely any time.
Set it up
Ready to run this against your own site? The setup guide takes you from install to a first replication, and the SharePoint connection guide covers signing in.
Script it
Everything you can click, you can script. The CLI, YAML config and automation below reproduce this scenario exactly. See the CLI reference.
spsync auth login --client-id 00000000-0000-0000-0000-000000000000
spsync export-config --output replication.yaml --pipeline-name sharepoint_postgresql_two_way_sync --prefix sharepoint_postgresql_two_way_sync --destination-kind postgres --connection-string-env SPSYNC_DB_CONNECTION --dataset-name sharepoint_postgresql_two_way_sync --client-id 00000000-0000-0000-0000-000000000000 --site-url https://contoso.sharepoint.com/sites/example --all-lists
spsync doctor replication.yaml
spsync run replication.yaml pipeline_name: sharepoint_postgresql_two_way_sync
prefix: sharepoint_postgresql_two_way_sync
auth:
mode: delegated
client_id: 00000000-0000-0000-0000-000000000000
tenant_id: common
destination:
kind: postgres
connection_string_env: SPSYNC_DB_CONNECTION
dataset_name: sharepoint_postgresql_two_way_sync
scopes:
- site_url: https://contoso.sharepoint.com/sites/example
mode: entire_site
reconcile_interval_days: 7
create_reporting_views: false
Cron (PostgreSQL nightly):
# every night at 02:00
0 2 * * * cd /opt/spsync && SPSYNC_DB_CONNECTION="$(cat /etc/spsync/db.env)" uv run spsync run replication.yaml >> /var/log/spsync/run.log 2>&1 GitHub Actions:
# .github/workflows/spsync-run.yml
name: SPSync
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch: {}
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync
- run: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: ${{ secrets.SPSYNC_DB_CONNECTION }}
SPSYNC_LICENCE_KEY: ${{ secrets.SPSYNC_LICENCE_KEY }} Azure DevOps:
# azure-pipelines.yml
schedules:
- cron: "0 2 * * *"
branches: { include: [main] }
always: true
steps:
- script: uv sync
- script: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: $(SPSYNC_DB_CONNECTION)
SPSYNC_LICENCE_KEY: $(SPSYNC_LICENCE_KEY) Windows (PowerShell + Task Scheduler):
# Windows Task Scheduler action (see /docs/cli/automation/)
$env:SPSYNC_DB_CONNECTION = (Get-Content C:\spsync\db.env)
uv run spsync run C:\spsync\replication.yaml
schtasks /Create /SC DAILY /ST 02:00 /TN "SPSync" /TR "powershell -File C:\spsync\run.ps1" macOS (launchd):
# ~/Library/LaunchAgents/io.spsync.run.plist, then: launchctl load ~/Library/LaunchAgents/io.spsync.run.plist Secrets (licence key, connection strings) come from the environment, so never write them into the YAML. See scheduling and secrets.