Last updated
Someone deletes or edits a SharePoint list item and there is no record of what it looked like beforehand, which is a problem the moment an auditor asks.
An auditor sampling purchase approvals asks about one that is no longer in the list. Someone deleted it while tidying up, and the evidence that it was ever approved went with it. SharePoint’s version history helps with edits, but it is hard to query in bulk and it goes when the item is deleted outright. An audit record has to survive the delete button.
A record that keeps deleted items
SPSync replicates the list into a database table you control, and soft delete is the
default. When an item is removed from SharePoint, its row stays in the table with
sp_deleted_at set to the time the deletion was seen. The approval the auditor asked
about would still be there, with every column it had on the day it was removed.
Each sync brings the table up to date with the list, so an edited field shows its new value after the next run. A reconcile, weekly by default, compares the whole list end to end, so you can confirm the archive matches SharePoint, or catch it up after an outage on your own server or in Microsoft 365.
What auditors can query
The lookup, choice and person columns come across, so the archive can be queried in full; managed metadata columns are not replicated yet. Lookups are key columns and multi-value columns are child tables. Auditors who do not want to write joins can be given a generated reporting view per list. Hard delete exists in the command-line tool for a database that should mirror SharePoint’s current state, but for an archive the soft-delete default is what you want.
| 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.
Where the archive sits, and who can read it
The archive is a database you run, with your own access rules and backup regime, independent of SharePoint’s retention settings. Restrict it to the people who need it and give auditors read access to the tables or the views. SPSync never writes to SharePoint, so archiving cannot alter the record being archived. Running the sync from SPSync in your Azure keeps it capturing changes outside office hours, and the command-line config can be kept under review in a pull request as part of a documented compliance process.
What it does not replace
SharePoint’s version history is not replicated. The table holds each item’s current values and every deleted item, which answers “what existed, and when was it removed”. For “what did this field say before Tuesday’s edit”, SharePoint’s own versioning is still the place to look.
The same soft-delete default is what makes it safe to shrink a list that has grown too big to use. The post on how to archive old SharePoint list items covers trimming closed items from SharePoint once the database has them, and compares that with Purview retention labels and an archive list.
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_archive_compliance --prefix sharepoint_archive_compliance --destination-kind postgres --connection-string-env SPSYNC_DB_CONNECTION --dataset-name sharepoint_archive_compliance --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_archive_compliance
prefix: sharepoint_archive_compliance
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_archive_compliance
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.