Last updated
One way: SharePoint stays the system of record, and SPSync keeps a read-only copy of your lists current in Neon.
Status: Shipped · product: SPSync for Relational Databases
Getting SharePoint list data into Neon usually starts as a script that calls Graph on a timer, and it breaks the first time a list passes 5,000 items or the compute is asleep when the script connects. SPSync takes that job over. It writes each list into a Neon branch as normalised Postgres tables and, after the first load, moves only the items that changed.
Neon separates storage from compute, so a database can sleep when no one queries it and be copied in seconds as a branch. Both properties change how you run a SharePoint replication. The sync wakes the database on its schedule, and a branch gives you a throwaway copy of the replicated tables for testing.
How do I connect SPSync to a Neon branch?
Each Neon branch has its own connection string, shown in the Connect dialog of the Neon
console. Neon offers two forms of it: a direct string, and a pooled one whose host
carries a -pooler suffix. Give SPSync the direct string. Neon’s pooler runs PgBouncer in
transaction mode, which drops session settings between transactions, and Neon’s own
advice is a direct connection for tools that create and alter tables. A sync does both on
its first run and again whenever someone adds a column to a list. The pooled string stays
useful for the app that reads the tables.
Paste the string unchanged. Some strings carry an options=endpoint%3D... parameter that
older client stacks need, and trimming it can break the connection. Neon requires TLS, so
keep sslmode=require on the end. The whole string, password included, belongs in an
environment variable on the machine that runs the sync; the replication’s YAML file holds
only the variable’s name.
SPSync signs in with the Neon role and password in that string. Create a role for the
sync and give your app a separate role for reading. The sync’s role needs two schemas it
owns: the one named by the replication’s dataset_name, where the tables live, and a
second with _staging on the end, where each run holds changed rows before merging
them. One Neon database can then hold the SharePoint copy beside your app’s own tables
without the two mixing.
SPSync talks to Neon through the same PostgreSQL destination it uses for a server you host yourself. Its automated tests run against stock PostgreSQL, and the Neon details on this page come from Neon’s documentation, so make your first run against a branch before you aim it at the one your reports read.
Scale to zero and the sync schedule
Neon suspends a compute after five minutes without activity and resumes it on the next connection, usually within a few hundred milliseconds. The sync’s retries absorb that pause, but avoid a very short connect timeout in the string. A run every five minutes keeps compute awake for most of the working day; a fifteen-minute or hourly interval lets it sleep between runs, which is cheaper on a usage-billed plan.
Two kinds of run take longer than the rest. The first load reads every item in every list you picked, and the reconcile, weekly by default and configurable per replication, compares the whole list with the table to catch anything a missed run or an outage left behind. Schedule a large first load for an evening if a dashboard shares the compute.
Branches for safe changes
A branch starts as a copy-on-write clone of its parent, so making one costs nothing until its data starts to differ. Two ways to use one:
- Try a configuration. Point a second replication at a branch with an added list or a different table prefix, check the result, then make the same change on the main branch.
- Try a downstream change. Build a new reporting view or dbt model against the branch’s copy of the tables before touching the one your dashboard reads.
A branch is a separate connection target. It stays current only if a replication writes to it, so a branch you test on drifts from SharePoint once you stop syncing it. Neon can reset a branch from its parent when you want a fresh copy, and a branch given an expiry date removes itself when the test is over.
Neon limits that shape the setup
- Storage per project. Every Neon plan caps storage. Each multi-value column adds a child table, and soft-deleted rows stay until you purge them, so size the project for the lists plus their history.
- Direct connections. The number of direct connections a compute accepts grows with its size. The sync connects only while a run is in progress, but on the smallest compute an app that also uses direct connections can run out first.
- Scale to zero has a ceiling. Neon applies it only to computes up to a certain size, so a large compute stays on and bills for the time.
- Identifier length. Postgres caps table and column names at 63 bytes. SPSync shortens long SharePoint names to fit and keeps them unique.
Worked example: a sales pipeline list on Neon
A sales site, replicated with the table prefix crm, has an Opportunities list with these
columns: Title, Account (a lookup to the site’s Accounts list), Owner (a person column),
Stage (a choice column), Amount (a number), Close date and Products (a multi-choice
column). Pick both lists in the replication and the first run creates:
| Table | What it holds |
|---|---|
crm_opportunities | One row per opportunity, keyed on sp_item_id: title, stage, amount as double precision, close_date as timestamptz, account_id, owner_user_id, and the sp_ columns such as sp_modified and sp_deleted_at |
crm_accounts | The Accounts list, keyed on sp_item_id, which account_id points at |
crm_opportunities__products | One row per product ticked: sp_item_id, value, ordinal |
sp_users | Everyone named in a person column: id, login, display_name, email |
There are no foreign key constraints between them. The key columns are there to join on, and you can add constraints yourself on a branch to see whether your app needs them. Open pipeline by product and owner, counting live rows only, reads like this:
select p.value as product, u.display_name as owner, count(*) as opportunities, sum(o.amount) as pipelinefrom crm_opportunities ojoin crm_opportunities__products p on p.sp_item_id = o.sp_item_idleft join sp_users u on u.id = o.owner_user_idwhere o.sp_deleted_at is null and o.stage not in ('Won', 'Lost')group by p.value, u.display_nameorder by pipeline desc;SharePoint cannot answer that in one view: the product split needs the multi-choice column split into rows, and the owner name needs a join that the list’s own API makes you do in code. Try the query on a branch first if you plan to turn it into a view that a dashboard reads.
Can an AI agent query the Neon copy?
Yes, through a Postgres MCP server you run and point at the copy under a read-only
role, or run spsync mcp serve, SPSync’s own read-only MCP server. On Neon, a sensible home for that connection is a read
replica on the same branch: it is a second compute over the same storage, so the agent’s
queries never compete with the sync’s writes and no data is duplicated. Give the agent its
own role:
create role pipeline_agent login password 'choose-a-long-one';grant usage on schema crm to pipeline_agent;grant select on all tables in schema crm to pipeline_agent;alter default privileges for role spsync_sync in schema crm grant select on tables to pipeline_agent;Here spsync_sync is the role in the sync’s connection string. The last statement covers
tables the sync creates later, such as a child table for a new multi-choice column. SharePoint item permissions are not copied into Neon, so this role,
plus row-level security if you need it, is what limits what the agent can read. The agent
never holds a Graph permission, and its questions cost SharePoint nothing.
Where Neon fits
Developers use a branch as the database behind a preview build of an app that reads SharePoint data. Report authors point a Power BI model at the main branch. Analysts join lists in SQL, and a multi-site consolidation can be tried on a branch before it is run for real.
A first run against Neon
- Create the sync’s role and a schema for it on a test branch.
- Use delegated sign-in for a first look at SharePoint; for scheduled runs, an admin
grants the app
Sites.Selectedon the sites involved. - Store the branch’s direct connection string in an environment variable.
- Choose lists, libraries or a whole site. The first run builds the tables and loads every item; later runs read changes only.
If the first connection fails with a message that the endpoint ID is not specified, the
client stack on the sync host lacks SNI support and needs the options=endpoint%3D...
parameter from Neon’s own string. A password error after a role reset in the console
means the environment variable still holds the old password.
Neon bills compute time and storage per project, separately from SPSync.
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.
Frequently asked questions
Should SPSync use Neon's pooled connection string or the direct one?
The direct one, without `-pooler` in the host. Neon's pooler runs PgBouncer in transaction mode, and Neon points tools that create or alter tables at a direct connection. Your app can keep using the pooled string to read the same tables.
Can I use a Neon branch to test a change before it reaches production?
Yes. Create a branch, point a second SPSync replication at it, and try a new configuration or a downstream schema change there. The branch your reports read is untouched.
Does SPSync work with scale-to-zero?
Yes. If Neon has suspended the compute, the sync's connection wakes it, as any client would, and the run carries on once the compute is back. Avoid a very short connect timeout in the connection string so the first attempt is not cut off.
Talk to us about Neon
SPSync is licensed per installation and per target family, with Standard Support included. Tell us which targets you need and how many machines, and we will size it with you. See how licensing works.
Set it up
Sign in to SharePoint with the SharePoint connection guide, then follow the PostgreSQL setup guide to create the database user and open the connection for Neon. If the connection test fails, the troubleshooting guide lists the errors people hit and what fixes them.
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 neon --prefix neon --destination-kind postgres --connection-string-env SPSYNC_DB_CONNECTION --dataset-name neon --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: neon
prefix: neon
auth:
mode: delegated
client_id: 00000000-0000-0000-0000-000000000000
tenant_id: common
destination:
kind: postgres
connection_string_env: SPSYNC_DB_CONNECTION
dataset_name: neon
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.