Last updated
An internal app or integration reads SharePoint through the Graph API directly, and every change to a list's schema or every throttling response risks breaking it.
The room booking app started as a weekend project. It reads the facilities team’s Rooms and Equipment lists straight from the Graph API, and for a year that was fine. Then the app gained users. Throttling responses began to need retry code, every request resolved the same lookup and person columns again, and one Monday a facilities manager changed a column. The API handed back a different shape and the app broke before the first meeting of the day.
A database the app already knows how to use
SPSync replicates the lists the app depends on into a database and refreshes that copy on a schedule. The app’s data layer goes back to being a SQL connection, with the pooling, indexes and joins that come with one, and the Graph API drops out of the request path entirely.
- 1ConnectSign in with Microsoft, or grant per-site app permissions.
- 2DiscoverPick sites, lists and libraries, or a whole site.
- 3MapSPSync reads each list's columns and creates the tables and keys.
- 4Full loadEvery item, paged the way Microsoft recommends, into the destination.
- 5Delta syncEvery few minutes only what changed moves: inserts, updates, deletes.
- 6ReconcileWeekly by default, or on demand with the command-line tool: a full comparison to confirm SharePoint and your database match, or to catch back up after an outage.
Steps 5 and 6 repeat for as long as the replication runs. Nothing is ever written to SharePoint.
Each list is a table with a stable primary key. A lookup is a key column, and a multi-value column is a child table, which is the shape an ORM or query layer expects from a relational database. Deleted items stay in the table with a flag set, so the app can tell that a room existed last week even though it has since been retired.
| Title | Client lookup | Tags multi-choice | Owner person | Due |
|---|---|---|---|---|
| Riverside | Acme Ltd | Design; Build | Ada Lovelace | 2026-11-30 |
| Harbour | Borealis plc | Build | Alan Turing | 2027-02-14 |
then sync
- id
- title
- client_id → clients.id
- owner_id → sp_users.id
- due
- is_deleted soft delete
- sp_modified
- id
- title
- …
- project_id
- tag
One row per value: Riverside has two.
- id
- display_name
One users table shared by every list.
When someone edits the list
This is where the app stops breaking on Mondays. A column added in SharePoint appears in the table on the next run. A removed column is kept, so a query that names it still runs, and renaming a column in SharePoint leaves its table name alone, because SPSync uses the internal name. On SQL Server, PostgreSQL, MySQL, ClickHouse, Oracle, SQLite and DuckDB, a column whose type changes gets a new versioned column and the old one keeps its history, so the shape under the app does not shift without warning.

Wiring it up for a service
A service needs a sync that runs without a person signed in. Have an admin grant app
permissions with Sites.Selected for just the sites the app reads, include the lists it
joins against as lookup targets, and choose a database the app’s data layer already
supports. Set the schedule to match how fresh the app’s reads need to be, then point
its queries at the replicated tables.
This is a read path. SPSync never writes back to SharePoint, so an app that also writes keeps doing that through its own integration. The replica sits on your network under your credentials, and exposing it to an app or an API raises the same questions as any other database you already run.
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 application_read_replica --prefix application_read_replica --destination-kind postgres --connection-string-env SPSYNC_DB_CONNECTION --dataset-name application_read_replica --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: application_read_replica
prefix: application_read_replica
auth:
mode: delegated
client_id: 00000000-0000-0000-0000-000000000000
tenant_id: common
destination:
kind: postgres
connection_string_env: SPSYNC_DB_CONNECTION
dataset_name: application_read_replica
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.