Last updated
The data an app needs already lives in SharePoint lists, but building the app against SharePoint's API directly means paginated calls, opaque lookup IDs and no way to move fast.
A list that has served as a member directory for years holds the right data for a member-facing app: search the directory, see your own record, get told when an event is added. The SharePoint list API in front of it is the wrong tool for that app. Its paged calls were never meant for an app’s data layer, it returns lookup and person columns as IDs, and it offers no row-level security, generated API, realtime feed or ORM mapping.
Build on Postgres, edit in SharePoint
SPSync replicates the lists into Supabase or Postgres on a schedule and keeps the copy current. The app builds on a plain Postgres schema. Supabase’s generated APIs, row-level security policies and realtime subscriptions on table changes all work against the replicated tables. The membership team keeps editing in SharePoint, and the app sees each change within a few minutes on the default five-minute schedule.
- 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.
The schema the app gets
Each list is a Postgres table with a stable primary key. A lookup is a key column with its
display value beside it, so the app’s queries join related tables on it. Multi-choice
columns are child tables. Person columns resolve into a shared sp_users table, which
helps when the app shows who owns or created a record; multi-person values are still being
confirmed on a live tenant. Deleted items keep
their row with sp_deleted_at set, and the app decides whether removed members are shown.
| 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.

Row-level security on replicated tables
“Members see only their own record” is a row-level security policy on the replicated table, written as for any other Supabase table, on top of the tables SPSync maintains. Include the lookup lists the data model joins against when you pick the lists, and set the schedule by how fresh the app needs to be.
When the app needs to write
SPSync only reads from SharePoint. An app that creates or edits records writes either to its own tables in Supabase or back to SharePoint through a separate integration. Many apps do both: profile preferences in the app’s own tables, directory data from the replica. A prototype can run its sync from the desktop app; once the app is in production, its data source should run independently of any one machine.
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 supabase_apps_on_sharepoint_data --prefix supabase_apps_on_sharepoint_data --destination-kind postgres --connection-string-env SPSYNC_DB_CONNECTION --dataset-name supabase_apps_on_sharepoint_data --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: supabase_apps_on_sharepoint_data
prefix: supabase_apps_on_sharepoint_data
auth:
mode: delegated
client_id: 00000000-0000-0000-0000-000000000000
tenant_id: common
destination:
kind: postgres
connection_string_env: SPSYNC_DB_CONNECTION
dataset_name: supabase_apps_on_sharepoint_data
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.