/

Data Integration

Best Way to Sync Notion to Postgres in 2026

Intergalactic Data Labs

โ€”

โ€”

12 min read

Table of contents

Summarize

Notion has no outbound sync, so getting a Notion database into Postgres means polling the Notion API with a connector and landing each database as a table. Filament, Airbyte, Fivetran, Estuary, dlt, Sling, and a plain script all run this route in October 2026. The Notion API decides what separates them, namely which objects get read, whether archived pages ever reach Postgres, and how the request budget is spent.

Approach

Examples

Incremental

Pricing unit

Connector platform

Filament, Airbyte, Fivetran, Estuary

Pages by last_edited_time

None, credits, MAR, GB

Code-first loader

dlt, Sling, ingestr, Meltano

Sling and Meltano only

Free library or CLI Pro token

Automation tool

Zapier, n8n

New items as events

Tasks or executions

Foreign data wrapper

Supabase Wrappers Notion FDW

Live query, no copy

Free, Apache 2.0

Your own script

notion-client plus psycopg

You build the cursor

Engineer time

What decides a Notion to Postgres sync

Six facts about the Notion API decide this route, and every tool in the table below is judged against them.

Fact

What the API does

Source

Sharing

A connection reads only pages shared with it, children included

Authorization

Objects

Databases, pages, blocks, users, and comments are separate endpoints

Block children

Cursor

Database queries filter on last_edited_time

Query a data source

Archived pages

Queries exclude archived pages by default

Query a data source

Request budget

180 or 600 requests per minute per connection, plus a workspace cap

Request limits

API version

2025-09-03 split databases into data sources

Upgrade guide

Sharing bites first. Notion's authorization page states that "If the page is not shared, any API requests made will respond with an error", and a connection sees a database only after someone adds it from the page menu. Page content is the second cost. Every page needs its own block children request, so a large workspace spends most of its budget on content rather than rows.

The budget changed this September, when Notion's request limits page moved to a per minute allowance by workspace plan with a Retry-After of at most 60 seconds, per the changelog. The version split matters too. A client still on the older API fails on any database that gains a second data source, per Notion's upgrade guide.

Which tools can sync a Notion database to Postgres?

Twelve paths run Notion to Postgres, and the connector platforms differ most on how deep they read page content and how they treat archived pages.

Tool

Notion objects

Incremental

Deletes

Pricing unit

License

Filament

Databases, pages, blocks one level, users

Pages

Not detected

None

Apache 2.0

Airbyte

Data sources, pages, blocks 30 levels, comments, users

Pages, blocks, comments

in_trash field only

Credits, 6 per million rows

ELv2

Fivetran

Databases, pages, blocks three levels, comments, users

Databases and pages

Re-import each sync

MAR

Proprietary

Estuary

Databases, pages, blocks, comments, users

Per binding

Not documented

GB plus per connector

BSL

dlt

Databases as tables

No, replace mode

Replace clears them

Free library

Apache 2.0

Sling

Databases, pages, blocks, comments, users

Databases and pages

Not documented

CLI Pro, 79 dollars a month

GPL-3.0

ingestr

One database per command

No

Replace clears them

Free

FSL-1.1

Meltano tap-notion

Users, search, pages, blocks

Search, client side

Not documented

Free

Apache 2.0

Zapier

New database items

Events only

None

Tasks

Proprietary

n8n

Any endpoint you wire

You build it

You build it

Executions

Sustainable Use

Supabase Wrappers

Pages, databases, blocks, users as foreign tables

Live query

Live query

Free

Apache 2.0

Script, notion2pg

Whatever you code

You build it

You build it

Engineer time

BSD or MIT

Tool

What you pay

Fastest schedule

Filament

Nothing, your own compute

Any cron

Airbyte Cloud

20 dollars a month with 5 credits, 5 dollars per extra credit

1 hour on Standard, 15 minutes on Plus

Fivetran

Monthly active rows, 500,000 MAR free

15 minutes on Standard

Estuary

50 cents per GB plus 100 dollars per connector a month

Continuous

Sling

CLI Pro at 79 dollars a month

Any cron

dltHub

Library free, dltHub 12,000 dollars a month

Any cron

Zapier

100 tasks a month free, then per task

Per event

Airbyte's certified Notion source is the deepest reader. Its docs cover "recursively nested child blocks up to 30 levels deep" plus comments and the new data sources stream, and credits for API sources are metered per million rows, per the credits guide. Its Postgres destination warns to keep volumes under 10 GB. The code is ELv2 and open source, so nothing here is the open source alternative to it.

Fivetran is the one platform that documents delete capture for Notion. Its connector page says it re-imports the block, comment, and user tables during every sync to catch updates and deletes, but it calls the connector Lite and caps the block hierarchy at three levels. Estuary reads the same five objects through a third party connector, per its capture docs, and ships under BSL.

The code-first loaders trade coverage for control. dlt's verified source loads each database as a table in replace mode and nothing else, per its docs, and the library stays Apache 2.0. Sling reads all five objects with incremental databases and pages, but its Notion page requires a CLI Pro token. ingestr copies one database per command in full, per its source page, under FSL-1.1.

Automation tools and the wrapper serve different jobs. Zapier's Notion to PostgreSQL templates insert a row per new item and bill per task, and n8n does the same inside a self-hosted workflow engine. The Supabase Wrappers FDW queries Notion live from SQL with no copy, and its docs warn that recursive block fetching is extremely slow. Hevo and Stitch have no Notion source, per the Hevo and Stitch catalogs.

How Filament handles Notion to Postgres

Filament reads Notion through an HTTP manifest and writes typed Postgres tables with primary keys, which meets the cursor, budget, and typing tests above and concedes deletes, depth, and the version pin. Both the Notion source and the Postgres sink carry a beta label on the connector pages, which is where they sit on the roadmap. Disclosure, this site belongs to Galaxy, the company behind Filament.

On the cursor, the Notion source page is explicit that "pages is the one incremental resource", filtered on last_edited_time with a 300 second overlap re-read each run. On the budget, the same page says "Selecting blocks therefore adds at least one API request per page in the workspace", and the engine backs off on 429 responses using Retry-After.

On typing, the Postgres sink creates each table from the source schema with id as the primary key, maps Notion's JSON to jsonb, and upserts with ON CONFLICT so a rerun is idempotent. An interrupted run resumes after the last completed page of a top level resource, and checkpoints carry incremental progress between runs, per the checkpoints guide.

The concessions are deletes, depth, and the version pin. The source sends no archived selector, and an incremental cursor cannot see a hard delete per the replication modes guide, so an archived page keeps its old row. The manifest reads one level of block children per page, pins the older API version, and skips comments. No public benchmark covers a Notion route, so no speed claim is made here.

Setting up the Notion to Postgres pipeline in Filament

The docs carry no Notion specific example, so the commands below apply the documented flag rule to the connector's one config key and were assembled, not copied. Validate them on v0.27.0 before relying on them.

Install the CLI from the CLI guide, create a Notion connection in Notion's settings, copy its secret, and share each database with it. The CLI rule is that every connector field becomes a flag with a source or sink prefix, so api_key becomes --source-api-key, and the env form keeps the secret out of shell history.

curl -fsSL https://getgalaxy.io/filament/install | sh

filament source create wiki \
  --source-connector notion \
  --source-api-key-env NOTION_API_KEY

filament sink create apps-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env APPS_DB_DSN

filament source

curl -fsSL https://getgalaxy.io/filament/install | sh

filament source create wiki \
  --source-connector notion \
  --source-api-key-env NOTION_API_KEY

filament sink create apps-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env APPS_DB_DSN

filament source

curl -fsSL https://getgalaxy.io/filament/install | sh

filament source create wiki \
  --source-connector notion \
  --source-api-key-env NOTION_API_KEY

filament sink create apps-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env APPS_DB_DSN

filament source

Discovery lists four resources. Only pages carries a cursor, so the clean split is two pipelines, a frequent incremental one for pages and a nightly full one for the small catalog tables. Blocks go in a third pipeline only when page content is needed, since every run re-reads them.

Resource

Read mode

Key

Columns worth knowing

databases

Full

id

title, last_edited_time, raw jsonb

pages

Incremental on last_edited_time

id

database_id, archived, url, properties jsonb

users

Full

id

name, type, person jsonb, bot jsonb

blocks

Full, one level per page

id

page_id, type, has_children, raw jsonb

filament pipeline create notion-pages \
  --source wiki --sink apps-db \
  --resources pages \
  --sync-mode incremental --write-mode upsert \
  --sink-schema notion

filament pipeline create notion-catalog \
  --source wiki --sink apps-db \
  --resources databases,users \
  --sync-mode full --write-mode upsert \
  --sink-schema

filament pipeline create notion-pages \
  --source wiki --sink apps-db \
  --resources pages \
  --sync-mode incremental --write-mode upsert \
  --sink-schema notion

filament pipeline create notion-catalog \
  --source wiki --sink apps-db \
  --resources databases,users \
  --sync-mode full --write-mode upsert \
  --sink-schema

filament pipeline create notion-pages \
  --source wiki --sink apps-db \
  --resources pages \
  --sync-mode incremental --write-mode upsert \
  --sink-schema notion

filament pipeline create notion-catalog \
  --source wiki --sink apps-db \
  --resources databases,users \
  --sync-mode full --write-mode upsert \
  --sink-schema

The same shape as a config file, validated with filament config validate per the first pipeline guide, looks like this.

version: 1

sources:
  wiki:
    type: notion
    config:
      api_key: env:NOTION_API_KEY

sinks:
  apps-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:APPS_DB_DSN

pipelines:
  notion-pages:
    source:
      ref: wiki
    sink:
      ref: apps-db
      config:
        schema: notion
    resources:
      - pages
    sync_mode: incremental
    write_mode

version: 1

sources:
  wiki:
    type: notion
    config:
      api_key: env:NOTION_API_KEY

sinks:
  apps-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:APPS_DB_DSN

pipelines:
  notion-pages:
    source:
      ref: wiki
    sink:
      ref: apps-db
      config:
        schema: notion
    resources:
      - pages
    sync_mode: incremental
    write_mode

version: 1

sources:
  wiki:
    type: notion
    config:
      api_key: env:NOTION_API_KEY

sinks:
  apps-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:APPS_DB_DSN

pipelines:
  notion-pages:
    source:
      ref: wiki
    sink:
      ref: apps-db
      config:
        schema: notion
    resources:
      - pages
    sync_mode: incremental
    write_mode

Verify from psql. The count should match the database view in Notion, and the properties column flattens with jsonb operators, because a select property arrives as an object with a name field, per the property values reference.

SELECT database_id, count(*) FROM notion.pages GROUP BY 1;

CREATE VIEW notion.tickets AS
SELECT id, url, last_edited_time,
       properties->'Status'->'select'->>'name' AS status
FROM notion.pages
WHERE database_id = '<database id>' AND NOT

SELECT database_id, count(*) FROM notion.pages GROUP BY 1;

CREATE VIEW notion.tickets AS
SELECT id, url, last_edited_time,
       properties->'Status'->'select'->>'name' AS status
FROM notion.pages
WHERE database_id = '<database id>' AND NOT

SELECT database_id, count(*) FROM notion.pages GROUP BY 1;

CREATE VIEW notion.tickets AS
SELECT id, url, last_edited_time,
       properties->'Status'->'select'->>'name' AS status
FROM notion.pages
WHERE database_id = '<database id>' AND NOT

Schedules live in the web app, which the web guide opens with filament up --detach. A cron for the pages pipeline every 15 minutes sits well inside the 180 request budget for a few thousand pages. Lookback is a per resource cursor override set in the app or API, and the manifests guide advises setting it generously and letting upsert absorb the repeats.

Pitfalls that bite every Notion sync

Four problems show up on this route regardless of tool.

  • Relations arrive truncated, because a page response holds at most 25 related page references and marks has_more when there are more, per the property values reference.

  • Archived rows linger in Postgres under any upsert sync, so schedule a full replace run or compare against a fresh database snapshot before trusting a count.

  • Block reads dominate the request budget, and a tool that recurses 30 levels on a deep wiki can take hours per run at 180 requests per minute.

  • A database that gains a second data source breaks every client on the 2022-06-28 API, which includes Filament, dlt, and Meltano's tap, per Notion's upgrade guide.

The pick for each kind of Notion workspace

A few internal databases that change daily go to Filament on a schedule. A whole workspace with comments and deep page trees goes to Airbyte, and a team already paying for Fivetran stays there for the delete handling.

Notion workload

Pick

Deciding factor

Internal tool databases, typed tables, your own Postgres

Filament

Incremental pages, primary keys, no row meter

Full workspace export with comments and nested content

Airbyte

30 level block recursion and comments stream

Archived pages must disappear downstream

Fivetran

Documented re-import of tables each sync

One database into a notebook or DuckDB

ingestr or dlt

One command, replace mode is enough

Row per new ticket into an existing app table

Zapier or n8n

Event shaped, no backfill needed

Ad hoc SQL over Notion with no copy

Supabase Wrappers

Live foreign tables, slow on blocks

Related guides

Frequently asked questions

How do you connect Notion to a Postgres database?

Create a Notion internal connection, share each database with it, then point a connector tool at the token and a Postgres DSN. The tool calls the search and database query endpoints, pages through results 100 at a time, and writes one table per Notion database. Notion itself only syncs inbound, so every path polls the API.

Is there a way to export a Notion database to Postgres without code?

Yes. Fivetran, Airbyte Cloud, and Estuary each offer a Notion source and a Postgres destination behind a web form, and Zapier or n8n can insert a row per new Notion item. The trade is price per row, credit, or task, and the automation tools only see new items, not history or page content.

How many requests per minute does the Notion API allow?

Since this September each connection gets 180 requests per minute on Free and Plus workspaces and 600 on Business and Enterprise, spent at any pace inside a one minute window. A separate per workspace budget is shared across all connections, so a sync can be throttled even when its own budget is fine.

Do archived Notion pages get deleted from Postgres when you sync?

Not on their own. The database query endpoint leaves archived pages out by default, so a page archived after it was synced simply stops appearing and the old row stays under an upsert. Fivetran re-imports several tables each sync to catch this, and other tools need a periodic full replace run.

Why are Notion pages missing after a sync?

Almost always sharing. A connection reads only the pages and databases that were shared with it from the page menu, and child pages inherit that access. The second cause is a relation to a database the connection cannot see, which arrives empty rather than failing.

What happens to Notion page properties in a Postgres table?

Most tools keep the property map as one JSON column, because Notion returns every property as a typed object with nested rich text, select, relation, or rollup values. In Postgres that lands as jsonb, and a view with arrow operators flattens the handful of properties a dashboard needs.

Which Notion objects does Filament sync to Postgres?

Databases, the pages inside each database, one level of blocks under each page, and workspace users, each as its own typed table with id as the primary key. Comments are not read. Only pages carries an incremental cursor, so databases, users, and blocks are read in full on every run.

How does Filament pick up only changed Notion pages?

It filters each database query on last_edited_time and stores one watermark for the whole pages resource after a successful run. The next run re-reads a 300 second overlap so edits in a database scanned early are not skipped, and upsert into Postgres absorbs the rows that overlap repeats.

More articles

Stay up to date with what weโ€™re building

Stay up to date with what weโ€™re building

Stay up to date with what weโ€™re building

Questions

Answered

FAQ

What does Galaxy do?

What is Filament?

What is enterprise context management?

What does working with Galaxy look like?

How do you handle security and compliance?

Why does Galaxy build in the open?

Own your knowledge stack

Own your knowledge stack

Copyright ยฉ 2026 Galaxy. All rights reserved.