SharePoint to Supabase

Build a web or mobile app on SharePoint list data: SPSync keeps Supabase Postgres tables current while staff go on editing in SharePoint.

Book a call

Book a call to talk through your use case, see further walkthroughs or demos, and set up a trial in your own environment.

Last updated

One way: SharePoint stays the system of record, and SPSync keeps a read-only copy of your lists current in Supabase.

Status: Shipped · product: SPSync for Relational Databases

Plenty of small systems began as a SharePoint list because it was the tool to hand: a product catalogue, a member directory, a request queue. When that data needs a proper app in front of it, Supabase provides the pieces the list API lacks, from generated REST and realtime APIs to row-level security and an ORM-friendly schema. SPSync keeps the Supabase tables current from the list, and SharePoint stays where the business edits.

Connecting to a Supabase project

Supabase exposes one database through three endpoints, and SPSync works with all three:

The shared pooler is reachable over IPv4 on every plan, which matters when the sync runs on an office network or a VM with no IPv6 route. Paid projects also get a dedicated pooler on the database machine itself, which runs in transaction mode only. SPSync always connects with TLS (sslmode=require). Supabase itself accepts plain connections to the database and both poolers until you turn on SSL enforcement, which is off by default, as its SSL enforcement guide explains. Switch it on under Database settings unless another client still needs a plain connection. Copy the pooler host from the project’s Connect dialog, since its prefix depends on where the project was provisioned. The connection string, with the database password, goes in an environment variable on the machine that runs the sync.

The sync writes to Supabase through the same PostgreSQL destination it uses for any server. What this page says about Supabase’s endpoints, grants and plans comes from Supabase’s documentation, so check the Connect dialog of your own project before the first run.

The one setting to get right: exposed schemas

Supabase grants the anon and authenticated roles full rights on new tables in the public schema, and its Data API serves any table in an exposed schema to holders of your publishable key. Combine those with a table that has row-level security switched off and anyone with the key can read the list, or write to it. So decide where the sync writes before the first run:

New tables keep appearing as SharePoint changes. A multi-choice column added next month becomes a new child table, and it starts with RLS off. Supabase’s Security Advisor flags a table in public without RLS, so check it after any run that added a table.

Tables your app can use

Take a request queue with a Department lookup, a Requester person column, a Stage choice and a multi-choice Tags column, on a site replicated with the prefix ops. SPSync writes these tables:

TableColumns that matter to the app
ops_requestssp_item_id (primary key), title, stage, department_id, requester_user_id, sp_modified, sp_deleted_at
ops_departmentsThe lookup’s target list, keyed on sp_item_id
ops_requests__tagssp_item_id, value, ordinal, one row per tag
sp_usersid, display_name, email, login for every person named in any list

Each table gets a primary key; the lookup and person columns are plain columns to join on, so add constraints yourself if your app or its policies expect them. Deleted items stay in the table with sp_deleted_at set, so the app decides whether to show them. Hard delete is a per-replication setting in the command-line tool’s YAML config. New SharePoint columns appear on the next run, removed ones stay, and a retyped column gets a new _v2 column beside the old one.

An RLS policy that lets signed-in staff read live requests for their own department, and the query a dashboard page runs, look like this:

alter table sharepoint.ops_requests enable row level security;
create policy "staff read their department"
on sharepoint.ops_requests for select
to authenticated
using (
sp_deleted_at is null
and department_id = (select department_id from public.staff where user_id = auth.uid())
);
select r.title, r.stage, u.display_name as requester, string_agg(t.value, ', ') as tags
from sharepoint.ops_requests r
left join sharepoint.sp_users u on u.id = r.requester_user_id
left join sharepoint.ops_requests__tags t on t.sp_item_id = r.sp_item_id
where r.sp_deleted_at is null
group by r.sp_item_id, r.title, r.stage, u.display_name
order by r.sp_item_id desc;

The public.staff table is yours: it maps a Supabase Auth user to a department, which SharePoint cannot express as a row filter. Lookups and person columns arrive as keys, and the policy and the query join on them.

Security for the app’s readers

Because SPSync’s role owns the tables it creates, row-level security does not block its writes. If the sync connects as the postgres role, that role bypasses RLS in any case. Turn RLS on for each replicated table and write policies for the roles your app signs users in as, the same way your other tables are secured. If you set FORCE ROW LEVEL SECURITY, include a policy for the sync’s role too.

SharePoint item permissions do not travel with the data. A list item only some staff can see in SharePoint is a plain row in Supabase, and the policies you write are the only thing deciding who reads it.

Agents and the Supabase copy

An AI agent can work from these tables the same way the app does. Point a Postgres MCP server at the session pooler with a login that can only SELECT the schema the sync writes to, or the views you choose to share. spsync mcp serve, SPSync’s own read-only MCP server, is another route. Keep the agent’s login separate from postgres and from the service role, both of which bypass row-level security, so the policies above apply to it as well. Rows from a document library carry file names and paths; the file contents themselves are not copied.

Supabase limits to check first

Where it goes from here

The full story of an app built on SharePoint data starts here. The same tables serve as an application read replica for an existing app, feed a Power BI report, and support joins across lists in SQL.

Steps

  1. Sign in with delegated access, or have an admin grant Sites.Selected on the site so the sync runs unattended.
  2. Create the target schema and decide whether it is exposed.
  3. Store the pooler connection string in an environment variable.
  4. Choose the lists or the whole site; the first run creates the tables and loads every item.
  5. Set a schedule, every few minutes up to hourly. Later runs read changes only, plus a reconcile, weekly by default, that checks every table against SharePoint.
  6. Turn RLS on and add policies, then connect the app.

Supabase plans cap database size and connections, billed by Supabase, so check both before replicating a large site.

Watch it run

Watch the full video

Chapters are in the video's description.

Watch the walkthrough: the Northwind Analytics demo lists in SharePoint, connecting to Microsoft 365 in the desktop app, picking lists, the run into an empty Supabase Postgres database, and the replicated tables in Supabase Studio.

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

Does SPSync work through Supabase's connection pooler?

Yes. SPSync defaults to the transaction pooler and can also use the session pooler or the direct connection. The default is our choice: the shared pooler is reachable over IPv4 on every Supabase plan, and a sync run connects, loads and disconnects, so holding a pooled connection only for each transaction suits it. Supabase's own guide describes transaction mode for serverless functions and the direct connection for migrations. The database library behind the sync sets the target schema as a connection startup option, and a run through the transaction pooler on a live Supabase project has not been tested yet. If the first run cannot connect, switch to the session pooler. SPSync connects with TLS in every mode.

Will row-level security get in the way of the sync?

No. SPSync creates the replicated tables, so its role owns them, and Postgres does not apply row-level security policies to a table's owner unless you force it. Add policies for the roles your app uses to read the tables.

Can an app use Supabase's generated APIs on the replicated tables?

Yes. Supabase's REST and realtime APIs work on them as on any other table in an exposed schema. If SPSync writes to a schema other than public, add that schema to the API's exposed schemas.

Talk to us about Supabase

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 Supabase. 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 supabase --prefix supabase --destination-kind postgres --connection-string-env SPSYNC_DB_CONNECTION --dataset-name supabase --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
prefix: supabase
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
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.

Book a call

Book a call to talk through your use case, see further walkthroughs or demos, and set up a trial in your own environment.