/

Data Integration

Best Way to Sync Slack to Postgres in 2026

Intergalactic Data Labs

—

—

13 min read

Table of contents

Summarize

If you run a community or support channel in Slack and want every message, thread, and member in a Postgres database you control, this guide is for you. The hard part is not Postgres. It is a Slack API that opens history only to bots that joined the channel, hides replies behind a second call, and since May 2025 throttles unlisted commercial apps to one history request a minute.

As of November 2026 four paths land Slack data in Postgres. A self-hosted engine such as Filament or Airbyte runs on your compute under your own app. A managed platform such as Hevo or Fivetran runs the sync and meters events or rows. A library such as dlt leaves scheduling to you, and a foreign data wrapper queries Slack live. App ownership, threads, deletes, and price decide between them.

Path

Examples

Best for

Main limit

Self-hosted engine

Filament, Airbyte

Typed tables on your own compute, no row meter

You run the process and the schedule

Managed platform

Hevo, Fivetran, Estuary, Stitch

Deletes handled for you, no servers

Metered by events, rows, or GB

Library or CLI

dlt, ingestr, Sling, Meltano

A pipeline that lives in a repo

Replies off by default or behind a paid token

Live foreign data wrapper

Supabase Wrappers

Ad hoc SQL over Slack

One channel per query, nothing stored

What does the Slack API let a sync tool read?

Every path calls the same Web API, so its rules shape every tool on this page. The rate limit docs evaluate requests “per method, per workspace” in one minute windows and answer a breach with HTTP 429 and a Retry-After header.

Method

What it returns

Internal app

Unlisted commercial app

conversations.history

Messages in one channel

Tier 3, 50 or more a minute, up to 999 a page

1 a minute, 15 a page since May 29, 2025

conversations.replies

One thread

Tier 3, up to 1,000 a page

1 a minute, 15 a page

users.conversations

Channels the token has joined

Tier 3

Tier 3

conversations.list

Every channel the token can see

Tier 2, 20 or more a minute

Tier 2

users.list

Every member, deactivated included

Tier 2

Tier 2

files.list

Files, page numbered

Tier 3

Tier 3

The last column reshaped this route. Slack’s May 2025 changelog cut history and replies to Tier 1 for apps sold outside the Marketplace. It states that “Internal customer-built applications are not impacted by these changes.” A token from an app you created keeps the full budget. A vendor’s unlisted app does not, which is why CloudQuery now says its “Slack source plugin no longer supports syncing message histories”.

Three more rules decide completeness. The history docs say “Only user tokens can access public channels they are not in”, so a bot reads only channels it has joined. Replies never appear in history, per the retrieving messages guide, which marks a parent by a thread_ts equal to its ts.

An edit keeps the original ts and a delete simply vanishes. Only the message_deleted event signals that “The original message will no longer return in history calls.” A sync that trusts a ts cursor sees neither.

What decides a Slack to Postgres sync?

Five criteria decide this route, and throughput is not one of them, because the per minute budget caps every tool at the same ceiling.

  • App ownership. Whether the sync runs on an app you created, which keeps Tier 3, or on a vendor’s unlisted app, which gets one history request a minute.

  • Thread coverage. Whether the tool reads conversations.replies at all, and how it learns that an old thread gained a reply.

  • Edits and deletes. Whether the tool re-reads a window of history and compares it with the destination, or trusts a ts cursor and never sees either.

  • Postgres landing. Typed columns keyed on channel and ts, blocks and reactions as jsonb, and an upsert so a rerun converges, per the write modes guide.

  • The unit you pay by. Events, monthly active rows, credits, gigabytes, a flat token, or your own compute.

Which tools can sync Slack to Postgres?

Thirteen tools run Slack to Postgres, and they split on whether they catch deletes and whether they read threads at all.

Tool

Slack objects

Incremental

Edits and deletes

Pricing unit

License

Filament

Team, users, groups, channels, messages, replies, files, bookmarks, pins

Messages, replies, files

Not observed

None

Apache 2.0

Hevo Edge

Users, channels, members, messages, replies, reactions, files

Messages, replies

Marked by comparison

Events

Proprietary

Fivetran Slack App

30 plus tables, messages, blocks, reactions, pins, bookmarks

Replies, 30 day history re-read

Monthly re-import

Monthly active rows

Proprietary

Airbyte

Channels, members, messages, users, threads

Messages, threads

Not documented

Credits on Cloud

ELv2

Estuary

Channels, members, messages, users, threads, groups, files

From a start date

Not documented

GB plus per connector

BSL 1.1

Stitch

Channels, members, messages, threads, files, users, groups

Messages, files, users

Threads re-read in full

Rows a month

Proprietary, community tap

dlt

Channels, messages, users, access logs

Messages

None

None

Apache 2.0

Sling

Team, users, channels, members, messages, threads

Messages

Not documented

CLI Pro token

GPLv3

ingestr

Channels, users, messages, access logs

Messages

None

None

FSL 1.1

Meltano tap-slack

Channels, members, messages, threads, users

Messages

Moved threads re-read

None

Apache 2.0

CloudQuery

Users, teams, channels, files, no history

None stated

None

Rows a year

MPL 2.0

Supabase Wrappers

Workspaces, channels, messages, users as foreign tables

Live query

Live query

None

Apache 2.0

Portable

Not itemized

Not stated

Not stated

Flat plan

Proprietary

Hevo is the one platform that documents delete detection for messages. Its Slack source page compares each fetch with the destination and sets __hevo__marked_deleted on rows the API no longer returns, though users and channels sit outside that check. It bills by events above a free tier, per its pricing page.

Fivetran’s classic Slack connector is sunset, and its changelog says every message table was removed for connections created on or after September 3, 2025. The replacement Slack App connector runs on an app you create, re-reads a month of history each sync, and re-imports monthly to catch updates and deletes, with the bot joining one channel at a time.

Airbyte’s certified source reads threads and private channels and offers a join all public channels toggle, per its docs. Airbyte is open source under ELv2. Its Postgres destination asks you to “only consider using postgres as a destination for small data volumes (e.g. less than 10GB) or for testing purposes”, and Cloud starts at 20 dollars a month.

Estuary runs the same third party connector with a threads lookback and bills “$0.50/GB + $100/mo per connector instance” above a free tier, per its pricing page, under the Business Source License.

Stitch runs the community Singer tap in beta and re-replicates a whole thread whenever one reply changes, per its integration page.

The code first tools trade coverage for control. dlt’s verified source keys a cursor on ts, keeps replies off by default, and stays Apache 2.0. ingestr has no replies table and ships under an FSL license. Sling covers threads behind a CLI Pro token, and Meltano’s tap-slack re-reads any thread whose latest reply moved.

The Supabase Slack wrapper queries Slack live rather than copying it, one channel per query. Snowflake’s Openflow connector for Slack lands in Snowflake only, and neither PeerDB nor Debezium has a Slack source.

Where Filament fits on Slack to Postgres

Filament reads Slack through an HTTP manifest under an app you create and writes typed Postgres tables, which meets the ownership, thread, and landing criteria and concedes deletes. The Slack source is alpha, a label the connector overview defines as documentation derived and not yet tested end to end, and the Postgres sink is beta. Filament is built by Galaxy, which also publishes this site.

On ownership, the source page tells you to use a token from an app you created in your own workspace, which keeps Tier 3. On threads, it lists channels through users.conversations, so a channel the bot never joined never raises not_in_channel, and it calls conversations.replies only for parents whose latest_reply passed the saved watermark.

On the landing, the sink page creates each table with a primary key from the source’s key fields, so messages key on channel_id and ts. It maps json to jsonb and upserts with ON CONFLICT (pk) DO UPDATE “so re-runs and retries are idempotent”. Rows arrive through binary COPY with a CRC32-C checksum per batch, per the integrity guide, and the engine is Apache 2.0 with no meter.

The concessions are on the page. The source states that “Edits and deletions of messages and replies are never observed”, so a deleted message keeps its row until a full replace. Every incremental run still walks the full history of every enabled channel to find threads that moved, and all channels share one messages cursor.

No public benchmark covers a Slack route, so this page carries no throughput claim.

Setting up Slack to Postgres with Filament

One Slack app, one bot token, one Postgres role, and two pipelines cover a workspace. The sink block is copied from the CLI guide. The source flags apply its --source-<field> rule to the connector’s two config keys, because the docs carry no Slack example yet, so validate them on v0.33.0.

Create the app at api.slack.com inside the workspace you want to read, which keeps it internal. Give the bot token the channels:history, channels:read, groups:history, groups:read, users:read, usergroups:read, files:read, pins:read, bookmarks:read, and team:read scopes, install it, and invite it to each channel with /invite.

Install the CLI from the installation page and create the connections. Secrets take an environment reference, which the CLI stores as env:NAME.

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

filament source create community \
  --source-connector slack \
  --source-token-env SLACK_BOT_TOKEN

filament sink create support-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env SUPPORT_DB_DSN

filament source

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

filament source create community \
  --source-connector slack \
  --source-token-env SLACK_BOT_TOKEN

filament sink create support-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env SUPPORT_DB_DSN

filament source

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

filament source create community \
  --source-connector slack \
  --source-token-env SLACK_BOT_TOKEN

filament sink create support-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env SUPPORT_DB_DSN

filament source

Discovery lists nine resources. Three carry a cursor, so the split is an hourly incremental pipeline for messages, thread replies, and files, and a nightly full pipeline for the catalog, both in upsert mode. Private channels go in through conversation_types in the config file below.

Resource

Read mode

Key

Lands as

messages

Incremental on ts

channel_id, ts

text, user, thread_ts, reply_count, blocks and reactions jsonb, raw

thread_replies

Incremental on ts

channel_id, thread_ts, ts

Same columns, one row per reply

files

Incremental on created

id

name, mimetype, size, user, raw

conversations, users, user_groups, team, bookmarks, pins

Full

id, or channel_id and created

Catalog tables with a raw remainder

filament pipeline create slack-activity \
  --source community --sink support-db \
  --resources messages,thread_replies,files \
  --sync-mode incremental --write-mode upsert \
  --sink-schema slack

filament pipeline create slack-catalog \
  --source community --sink support-db \
  --resources team,users,user_groups,conversations,bookmarks,pins \
  --sync-mode full --write-mode upsert \
  --sink-schema

filament pipeline create slack-activity \
  --source community --sink support-db \
  --resources messages,thread_replies,files \
  --sync-mode incremental --write-mode upsert \
  --sink-schema slack

filament pipeline create slack-catalog \
  --source community --sink support-db \
  --resources team,users,user_groups,conversations,bookmarks,pins \
  --sync-mode full --write-mode upsert \
  --sink-schema

filament pipeline create slack-activity \
  --source community --sink support-db \
  --resources messages,thread_replies,files \
  --sync-mode incremental --write-mode upsert \
  --sink-schema slack

filament pipeline create slack-catalog \
  --source community --sink support-db \
  --resources team,users,user_groups,conversations,bookmarks,pins \
  --sync-mode full --write-mode upsert \
  --sink-schema

The same shape as a config file, checked with filament config validate per the first pipeline guide, adds private channels.

version: 1

sources:
  community:
    type: slack
    config:
      token: env:SLACK_BOT_TOKEN
      conversation_types: [public_channel, private_channel]

sinks:
  support-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:SUPPORT_DB_DSN

pipelines:
  slack-activity:
    source:
      ref: community
    sink:
      ref: support-db
      config:
        schema: slack
    resources:
      - messages
      - thread_replies
      - files
    sync_mode: incremental
    write_mode

version: 1

sources:
  community:
    type: slack
    config:
      token: env:SLACK_BOT_TOKEN
      conversation_types: [public_channel, private_channel]

sinks:
  support-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:SUPPORT_DB_DSN

pipelines:
  slack-activity:
    source:
      ref: community
    sink:
      ref: support-db
      config:
        schema: slack
    resources:
      - messages
      - thread_replies
      - files
    sync_mode: incremental
    write_mode

version: 1

sources:
  community:
    type: slack
    config:
      token: env:SLACK_BOT_TOKEN
      conversation_types: [public_channel, private_channel]

sinks:
  support-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:SUPPORT_DB_DSN

pipelines:
  slack-activity:
    source:
      ref: community
    sink:
      ref: support-db
      config:
        schema: slack
    resources:
      - messages
      - thread_replies
      - files
    sync_mode: incremental
    write_mode

Verify from psql. The first query shows a channel the bot never joined as a missing row, the join proves threads landed, and a GIN index makes the archive searchable, per the full text search docs.

SELECT c.name, count(*) FROM slack.messages m
JOIN slack.conversations c ON c.id = m.channel_id GROUP BY 1 ORDER BY 2 DESC;

SELECT m.text AS parent, count(r.ts) AS replies
FROM slack.messages m
LEFT JOIN slack.thread_replies r ON r.channel_id = m.channel_id AND r.thread_ts = m.ts
WHERE m.reply_count > 0 GROUP BY m.channel_id, m.ts, m.text;

CREATE INDEX messages_text_fts ON slack.messages
USING gin (to_tsvector('english', text))

SELECT c.name, count(*) FROM slack.messages m
JOIN slack.conversations c ON c.id = m.channel_id GROUP BY 1 ORDER BY 2 DESC;

SELECT m.text AS parent, count(r.ts) AS replies
FROM slack.messages m
LEFT JOIN slack.thread_replies r ON r.channel_id = m.channel_id AND r.thread_ts = m.ts
WHERE m.reply_count > 0 GROUP BY m.channel_id, m.ts, m.text;

CREATE INDEX messages_text_fts ON slack.messages
USING gin (to_tsvector('english', text))

SELECT c.name, count(*) FROM slack.messages m
JOIN slack.conversations c ON c.id = m.channel_id GROUP BY 1 ORDER BY 2 DESC;

SELECT m.text AS parent, count(r.ts) AS replies
FROM slack.messages m
LEFT JOIN slack.thread_replies r ON r.channel_id = m.channel_id AND r.thread_ts = m.ts
WHERE m.reply_count > 0 GROUP BY m.channel_id, m.ts, m.text;

CREATE INDEX messages_text_fts ON slack.messages
USING gin (to_tsvector('english', text))

Every row also carries _filament_run_id, so a query can exclude a failed run. Schedules live in the web app, which filament up --detach opens per the web guide. Per the source docs, “Configure a lookback of at least one run’s duration on messages and thread_replies if late arrivals matter.” That override lives per resource in the pipelines API, and upsert absorbs the repeats.

Which path fits which Slack workspace?

A community or support workspace that wants typed tables on its own Postgres goes to Filament under its own app. A team that needs deleted messages marked goes to Hevo, and a team already on Fivetran stays there for the monthly re-import.

Slack workload

Path

Criterion that decides

Support or community channels, typed tables, your own Postgres

Filament

Own app keeps Tier 3, replies by watermark, no meter

Deleted messages must disappear downstream

Hevo Edge

Documented comparison that marks deletes

Already paying Fivetran, want edits caught

Fivetran Slack App

30 day re-read and monthly re-import

Many public channels, no one to invite the bot

Airbyte

Join all public channels toggle

Minutes fresh messages under 10 GB a month

Estuary

Free tier covers it, threads lookback

One channel into a notebook

dlt or ingestr

One command, cursor on ts

Ad hoc SQL over live Slack, nothing stored

Supabase Wrappers

Foreign tables, one channel per query

Hevo wins when deletes must propagate, Airbyte when nobody will invite a bot to hundreds of channels, and Estuary on freshness under its free gigabytes. Filament takes the typed table case where the data stays on your compute, as on the Notion, GitHub, and PostHog routes into the same sink. It loses the moment a deleted message has to vanish without a full replace.

Frequently asked questions

How do you export Slack messages to a Postgres database?

Create a Slack app in your own workspace, give it the history and read scopes, invite its bot to each channel, then point a sync tool at the bot token and a Postgres connection string. The tool walks conversations.history per channel, calls conversations.replies for threads, and writes one table per object.

What is the Slack API rate limit for reading message history?

For an app built in your own workspace, conversations.history and conversations.replies sit at Tier 3, which Slack documents as 50 or more requests a minute with hundreds of messages per page. Since May 2025 a commercially distributed app outside the Slack Marketplace gets one request a minute and 15 messages per page on those two methods.

Does a Slack sync pick up edited and deleted messages?

Not through history alone. Slack keeps the original ts on an edited message and drops a deleted one from history without a marker, so a cursor on ts never sees either. Tools catch them by re-reading a window of history and comparing it with the destination, or by subscribing to the message_changed and message_deleted events.

Why does a Slack sync return not_in_channel for some channels?

A bot token reads history only for conversations the bot has joined. Slack’s docs say only user tokens can access public channels they are not in, so a bot that can list a channel still gets not_in_channel on its history until someone invites it. Private channels always need an invitation.

Are thread replies included in Slack conversation history?

No. conversations.history returns the parent message with a thread_ts and a reply count, and the replies sit behind conversations.replies, one call per thread. Slack reveals new replies on an old thread only through the parent’s latest_reply, so a sync re-reads parents to learn which threads moved.

Can Slack send data to a database without a third party tool?

Only in narrow cases. Admins can export public channel messages as JSON on every plan, and private channels and direct messages on Business+ and Enterprise. Enterprise org owners get a Discovery API that Slack restricts to security and compliance uses. Snowflake’s Openflow connector for Slack lands in Snowflake, not Postgres.

Which Slack objects does Filament load into Postgres?

Team, users, user groups, conversations, messages, thread replies, files, bookmarks, and pins, each as a typed table with the unprojected JSON kept in a jsonb column. Messages, thread replies, and files read incrementally, and the rest are re-read in full every run. Edits and deletions are not observed.

How often should a Slack to Postgres pipeline run?

Hourly for messages and threads, nightly for users, channels, and files. Every incremental run re-walks the full history of each enabled channel to find threads that moved. Filament’s docs put that walk at about one request per thousand messages, so a busy workspace spends much of its budget on it.

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.