From 22284f1786c4ee5d945939143167dcdac7fccc12 Mon Sep 17 00:00:00 2001
From: Joshen Lim
Date: Mon, 27 Jul 2026 18:50:38 +0800
Subject: [PATCH 1/9] Use table for roles tooltip instead (#48353)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Context
Opting to use native `table` element instead for the roles tooltip in
`DatabaseConnections` to better handle varying role name lengths
### Before
### After
## Summary by CodeRabbit
* **Style**
* Improved the layout and readability of the “Connections by roles”
tooltip in database observability metrics.
* Role labels and connection counts are now presented in a clearer
tabular format.
---
.../DatabaseConnections/Overview.tsx | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx
index d8224a1ffa73d..fb7129bb5a495 100644
--- a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx
+++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx
@@ -135,12 +135,19 @@ export const Overview = ({ live }: OverviewProps) => {
-
Connections by roles:
- {rolesWithActiveConnections.map((role) => (
-
-
{role.name}:
{role.activeConnections}
-
- ))}
+
Connections by roles:
+
+
+ {rolesWithActiveConnections.map((role) => (
+
+
+ {role.name}:
+
+
{role.activeConnections}
+
+ ))}
+
+
}
>
From 6058ee7962187eaad8e087a29a7c7f14c64e5df5 Mon Sep 17 00:00:00 2001
From: Joshen Lim
Date: Mon, 27 Jul 2026 18:50:56 +0800
Subject: [PATCH 2/9] Add focus states for spans in overview cards (#48354)
## Context
Tiny one to address for a11y stuff for the spans in the metric cards for
database connections overview section
## Summary by CodeRabbit
* **Style**
* Improved hover and keyboard-focus styling for process ID details in
database observability metrics.
* Added a pointer cursor and smoother visual transitions to make
interactive details easier to identify.
---
.../Observability/DatabaseConnections/Overview.tsx | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx
index fb7129bb5a495..375aa984c77fb 100644
--- a/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx
+++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/Overview.tsx
@@ -239,7 +239,11 @@ export const Overview = ({ live }: OverviewProps) => {
setSelectedPid(longestBlockedQuery.activity.pid)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -289,7 +293,7 @@ export const Overview = ({ live }: OverviewProps) => {
setSelectedPid(queryBlockingTheMostQueries.activity.pid)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -346,7 +350,7 @@ export const Overview = ({ live }: OverviewProps) => {
setSelectedPid(longestRunningQuery.activity.pid)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
From 6f8fe470a688749c634bfbd923cfc3e6899049ff Mon Sep 17 00:00:00 2001
From: Riccardo Busetti
Date: Mon, 27 Jul 2026 12:52:37 +0200
Subject: [PATCH 3/9] ref(pipelines): Update pricing descriptions given new
egress calculation (#48241)
---
.../billing/pricing/pricing_pipelines.mdx | 16 +++---
.../database/replication/pipelines-faq.mdx | 8 +--
.../guides/database/replication/pipelines.mdx | 4 +-
.../platform/manage-your-usage/pipelines.mdx | 35 +++++++++++--
.../Replication/BatchRestartDialog.tsx | 25 ++++-----
.../DestinationForm/AdvancedSettings.tsx | 4 +-
.../DestinationForm/DestinationForm.utils.ts | 2 +-
.../PipelineCostDialog.test.tsx | 15 ++++--
.../DestinationForm/PipelineCostDialog.tsx | 32 ++++++++----
.../TableCopySelection.test.tsx | 10 ++--
.../DestinationForm/TableCopySelection.tsx | 52 ++++++++++---------
.../Replication/EnablePipelinesCallout.tsx | 4 +-
.../RestartCostEstimate.render.test.tsx | 39 ++++++++------
.../Replication/RestartCostEstimate.tsx | 18 ++++---
.../Replication/RestartTableDialog.tsx | 12 ++---
.../BillingBreakdown.constants.ts | 4 +-
.../Organization/Usage/Usage.constants.tsx | 8 +--
...5-12-02-introducing-supabase-pipelines.mdx | 2 +-
...-07-21-supabase-pipelines-public-alpha.mdx | 6 +--
.../components/Pricing/PricingTableRow.tsx | 2 +-
packages/shared-data/pricing.ts | 8 +--
21 files changed, 190 insertions(+), 116 deletions(-)
diff --git a/apps/docs/content/_partials/billing/pricing/pricing_pipelines.mdx b/apps/docs/content/_partials/billing/pricing/pricing_pipelines.mdx
index 48072976687d8..e2e8be44f74e6 100644
--- a/apps/docs/content/_partials/billing/pricing/pricing_pipelines.mdx
+++ b/apps/docs/content/_partials/billing/pricing/pricing_pipelines.mdx
@@ -1,11 +1,13 @@
{/* prettier-ignore */}
- per hour for each configured pipeline. per Gigabyte of initial sync data. per Gigabyte of ongoing replication data.
+ per hour for each configured pipeline. per Gigabyte of data processed during initial sync. per Gigabyte of data processed during ongoing replication.
-| Plan | Configured Pipeline | Initial Sync Data | Ongoing Replication Data |
-| ---------- | -------------------------- | ----------------------------- | ----------------------------- |
-| Free | - | - | - |
-| Pro | /hr | per GB | per GB |
-| Team | /hr | per GB | per GB |
-| Enterprise | Custom | Custom | Custom |
+| Plan | Configured Pipeline | Initial Sync Data Processed | Ongoing Replication Data Processed |
+| ---------- | -------------------------- | ----------------------------- | ---------------------------------- |
+| Free | - | - | - |
+| Pro | /hr | per GB | per GB |
+| Team | /hr | per GB | per GB |
+| Enterprise | Custom | Custom | Custom |
+
+**Data processed** is Postgres row data successfully processed by a pipeline and accepted by its destination. It is measured from the logical row data emitted by Postgres for replication, rather than physical table storage or destination-specific encoding.
For a detailed breakdown of how charges are calculated, refer to [Manage Pipeline usage](/docs/guides/platform/manage-your-usage/pipelines).
diff --git a/apps/docs/content/guides/database/replication/pipelines-faq.mdx b/apps/docs/content/guides/database/replication/pipelines-faq.mdx
index f01524856aa6e..e1deff65ff0dd 100644
--- a/apps/docs/content/guides/database/replication/pipelines-faq.mdx
+++ b/apps/docs/content/guides/database/replication/pipelines-faq.mdx
@@ -142,13 +142,13 @@ Open [**Database > Replication**](/dashboard/project/_/database/replication), cl
`Lost` means Postgres has already removed WAL files that the pipeline's replication slot needed. The pipeline cannot continue from that slot.
-You can recreate the pipeline, or open the pipeline's **Advanced settings**, set **Invalidated slot behavior** to **Recreate**, and start the pipeline again. The pipeline resets its saved table-sync state, creates a new replication slot, and replaces each destination table through a new initial sync. This destructive restart is required for consistency because the old slot can no longer provide every change the pipeline missed, and the new initial sync is billed as initial sync data.
+You can recreate the pipeline, or open the pipeline's **Advanced settings**, set **Invalidated slot behavior** to **Recreate**, and start the pipeline again. The pipeline resets its saved table-sync state, creates a new replication slot, and replaces each destination table through a new initial sync. This destructive restart is required for consistency because the old slot can no longer provide every change the pipeline missed, and the data processed during the new initial sync is billed again.
See [Slot statuses](/docs/guides/database/replication/pipelines-monitoring#slot-statuses) for all slot states and what to do next.
## Why is a table in error state?
-Table errors occur during the initial sync. To recover, click **View pipeline**, find the affected table, and click its restart action. This restarts that table's initial sync from the beginning, deletes its existing destination data, and bills the copied data as new initial sync data.
+Table errors occur during the initial sync. To recover, click **View pipeline**, find the affected table, and click its restart action. This restarts that table's initial sync from the beginning, deletes its existing destination data, and bills the successfully processed row data again.
## How to verify replication is working
@@ -209,7 +209,9 @@ Removing a table from the publication and restarting the pipeline does not delet
## Can data be processed more than once?
-Yes. Pipelines uses at-least-once processing. If a destination accepted a batch but the pipeline did not persist its checkpoint before a retry, restart, or interruption, that batch can be processed again.
+Yes. Pipelines uses at-least-once processing. Failed destination write attempts that Pipelines retries are not counted. Data is counted only after the destination acknowledges successful processing.
+
+In rare cases, Pipelines can count an acknowledged batch but crash or be interrupted before its replication checkpoint is persisted. Recovery can then process and count the same data again.
BigQuery uses the replicated source primary key and CDC ordering metadata to converge on the current table state. Pipelines does not provide a history of each delivery attempt that you can query or guarantee exactly-once event processing.
diff --git a/apps/docs/content/guides/database/replication/pipelines.mdx b/apps/docs/content/guides/database/replication/pipelines.mdx
index 75bee3a3dcff7..3812e0a658833 100644
--- a/apps/docs/content/guides/database/replication/pipelines.mdx
+++ b/apps/docs/content/guides/database/replication/pipelines.mdx
@@ -199,7 +199,7 @@ Follow these steps to configure your destination. Each destination has its own s
Leave these settings at their defaults unless you need to tune initial sync speed, latency, or recovery behavior.
- Use **Invalidated slot behavior** carefully. If **Recreate** is selected and the pipeline starts after Postgres has invalidated the main replication slot, the pipeline resets its saved table-sync state, creates a new slot, and replaces each destination table through a new initial sync. This destructive restart is required for consistency because the old slot can no longer provide every change the pipeline missed, and the new initial sync is billed as initial sync data.
+ Use **Invalidated slot behavior** carefully. If **Recreate** is selected and the pipeline starts after Postgres has invalidated the main replication slot, the pipeline resets its saved table-sync state, creates a new slot, and replaces each destination table through a new initial sync. This destructive restart is required for consistency because the old slot can no longer provide every change the pipeline missed, and the data processed during the new initial sync is billed again.
7. Click **Create and start pipeline** to begin replication
@@ -329,7 +329,7 @@ Pipelines has the following limitations:
- **Replica identity**: Requirements are destination-specific. Updates and deletes need enough row identity to apply safely. See [BigQuery source table requirements](/docs/guides/database/replication/bigquery#source-table-requirements) for the supported modes.
- **Schema changes**: Currently in beta and limited to BigQuery
- **No user-defined transformations**: Pipelines performs destination-compatible type and name mapping, but doesn't run custom transformations
-- **At-least-once processing**: A retry, restart, or interruption can cause a batch to be processed again. BigQuery uses primary-key-based CDC to converge on the current table state. See [Can data be processed more than once?](/docs/guides/database/replication/pipelines-faq#can-data-be-processed-more-than-once) for details.
+- **At-least-once processing**: Failed destination write attempts that Pipelines retries are not counted. In rare cases, Pipelines can count an acknowledged batch but crash or be interrupted before its replication checkpoint is persisted. Recovery can then process and count that batch again. BigQuery uses primary-key-based CDC to converge on the current table state. See [Can data be processed more than once?](/docs/guides/database/replication/pipelines-faq#can-data-be-processed-more-than-once) for details.
Destination-specific limitations, such as BigQuery's row size limits, are documented in each destination guide.
diff --git a/apps/docs/content/guides/platform/manage-your-usage/pipelines.mdx b/apps/docs/content/guides/platform/manage-your-usage/pipelines.mdx
index c468004e748f2..fb3585f34f922 100644
--- a/apps/docs/content/guides/platform/manage-your-usage/pipelines.mdx
+++ b/apps/docs/content/guides/platform/manage-your-usage/pipelines.mdx
@@ -5,14 +5,41 @@ title: 'Manage Pipelines usage'
## What you are charged for
-You are charged for configured pipelines, initial sync data, and ongoing replication data. Pipelines are charged by the hour for as long as they are configured, including while they are stopped.
+You are charged for configured pipelines and pipeline data processed. Data processed is billed at different rates during initial sync and ongoing replication. Pipelines are charged by the hour for as long as they are configured, including while they are stopped.
- **Pipeline hours** measure how long each pipeline remains configured. Delete a pipeline to end this charge.
-- **Initial sync data** is the volume of destination-write data processed when a table is first synchronized or synchronized again.
-- **Ongoing replication data** is the volume of destination-write data processed for subsequent database changes. It depends on how much your published data changes, not on the source table size, WAL size, or destination's compressed storage size.
+- **Initial sync data processed** is the Postgres row data accepted by the destination when a table is first synchronized or synchronized again.
+- **Ongoing replication data processed** is the Postgres row data accepted by the destination for subsequent database changes. It depends on how much your published data changes, not on the source table size, WAL size, or destination's compressed storage size.
Destination-provider charges are separate. For example, Google Cloud can charge for BigQuery ingestion, storage, and CDC compute.
+## How data processed is measured
+
+Pipeline data processed is the amount of logical row data emitted by Postgres for replication, successfully processed by a pipeline, and accepted by its destination. It is not based on physical table storage or destination-specific encoding, making usage consistent across destinations.
+
+The measurement includes:
+
+- **Initial sync and resynchronization**: Row data emitted by Postgres COPY.
+- **Ongoing replication**: Row values Postgres emits for inserts, updates, and deletes. Updates include new row values and any previous identity values Postgres emits. Deletes include the emitted identity values.
+
+Failed destination write attempts that Pipelines retries are not counted. Data is counted only after the destination acknowledges successful processing. In rare cases, Pipelines can count an acknowledged batch but crash or be interrupted before its replication checkpoint is persisted. Recovery can then process and count the same data again.
+
+Data successfully processed again as part of a user-requested resynchronization, table restart, or pipeline reset is also counted again.
+
+### Cost estimates
+
+The Dashboard provides a quick planning estimate of initial sync volume and cost using information already available about your source tables. It is designed to give you a useful indication before initial sync begins without first scanning and encoding all the data that the sync will process.
+
+If an estimate is unavailable, you can still create the pipeline or restart tables.
+
+
+
+Use this estimate as a planning guide rather than an exact quote. The final volume is measured from the data successfully processed during initial sync and can vary based on your published data and filters.
+
+
+
+Actual charges use the logical Postgres row data copied after publication column and row filters and accepted by the destination.
+
### Usage on your invoice
Usage is shown as "ETL Pipeline Hours", "ETL Copy Backfill Data GB", and "ETL Replicated Data GB" on your invoice.
@@ -39,7 +66,7 @@ The project has a configured pipeline for the entire month.
### Multiple projects with initial syncs
-Multiple projects had a configured pipeline for the entire month and processed initial sync data and ongoing replication data.
+Multiple projects had a configured pipeline for the entire month and processed data during initial sync and ongoing replication.
| Line Item | Units | Costs |
| ----------------------------------- | --------- | ----------------------------- |
diff --git a/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx b/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx
index 134849a32f089..5c9b209f0fec2 100644
--- a/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/BatchRestartDialog.tsx
@@ -59,28 +59,29 @@ export const BatchRestartDialog = ({
[affectedTables, tableSyncCopy]
)
- const initialCopyDescription =
+ const initialSyncDescription =
copiedTables.length === 0 ? (
- No table will run an initial copy. New changes will resume streaming
- without backfilling existing source rows. There is no additional initial-copy charge.
+ No table will run an initial sync. Replication will resume with new changes
+ only, without syncing existing source rows. There is no additional initial sync charge.
- {copiedTables.length === 1 ? 'The table' : `All ${copiedTables.length} tables`} will run
- an initial copy.
+ {copiedTables.length === 1
+ ? 'The table will run its initial sync again.'
+ : `All ${copiedTables.length} tables will run initial sync again.`}
{' '}
- Existing rows will be copied again from the source and billed in addition to previous
- initial copies.
+ Existing source rows will be synced again. Data successfully processed during this initial
+ sync is billed again.
) : (
- {copiedTables.length} of {affectedTables.length} tables will run an initial copy.
+ {copiedTables.length} of {affectedTables.length} tables will run initial sync again.
{' '}
- The remaining tables will resume streaming without a backfill. Copied rows are billed in
- addition to previous initial copies.
+ Existing source rows for those tables will be synced again and billed again. The remaining
+ tables will resume replication with new changes only.
)
@@ -127,7 +128,7 @@ export const BatchRestartDialog = ({
{affectedTables.length === 1 ? '' : 's'} in this pipeline from scratch:
All downstream data will be deleted. All replicated data will be
removed.
@@ -150,7 +151,7 @@ export const BatchRestartDialog = ({
{affectedTables.length} currently failed tables from scratch:
Existing downstream data will be deleted. Replicated data for
these tables will be removed.
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx
index 65945d1106836..d004564c635a1 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx
@@ -118,9 +118,9 @@ export const AdvancedSettings = ({
name="maxCopyConnectionsPerTable"
render={({ field }) => (
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationForm.utils.ts b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationForm.utils.ts
index a4b51e46829dc..3ddee07c982ae 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationForm.utils.ts
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/DestinationForm.utils.ts
@@ -172,7 +172,7 @@ export const buildTableSyncCopyConfig = ({
if (mode === 'include_all_tables' || mode === 'skip_all_tables') return { type: mode }
if (selectedTableIds.length === 0) {
- throw new Error('Select at least one table for the initial copy')
+ throw new Error('Select at least one table for the initial sync')
}
const tableIds = selectedTableIds.map(Number)
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.test.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.test.tsx
index 562b005dadbf2..d7020cc3ec48e 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.test.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.test.tsx
@@ -88,7 +88,7 @@ describe('PipelineCostDialog', () => {
costEstimateTables = tables
})
- it('shows only the initial-copy tables selected by the policy', async () => {
+ it('shows only the tables selected for initial sync', async () => {
renderDialog({ type: 'include_tables', table_ids: [101] })
expect(await screen.findByText('public.orders')).toBeInTheDocument()
@@ -97,10 +97,10 @@ describe('PipelineCostDialog', () => {
expect(screen.queryByText('$10.00')).not.toBeInTheDocument()
})
- it('shows a zero initial-copy charge while retaining ongoing rates', async () => {
+ it('shows a zero initial sync charge while retaining ongoing replication rates', async () => {
renderDialog({ type: 'skip_all_tables' })
- expect(await screen.findByText(/No tables will be initially copied/)).toBeInTheDocument()
+ expect(await screen.findByText(/No tables will run an initial sync/)).toBeInTheDocument()
expect(screen.getByText('$0.00')).toBeInTheDocument()
expect(screen.getByText('$0.05/hour')).toBeInTheDocument()
expect(screen.getByText('$3.00/GB')).toBeInTheDocument()
@@ -118,4 +118,13 @@ describe('PipelineCostDialog', () => {
expect(screen.getByText('Unavailable')).toBeInTheDocument()
expect(screen.queryByText('$0.60')).not.toBeInTheDocument()
})
+
+ it('explains that row filters can reduce processed data compared with the estimate', async () => {
+ costEstimateTables = [{ ...tables[0], is_row_filtered: true }]
+
+ renderDialog({ type: 'include_tables', table_ids: [101] })
+
+ expect(await screen.findByText(/Row filters can reduce the data processed/)).toBeInTheDocument()
+ expect(screen.getByText(/Quick planning estimate/)).toBeInTheDocument()
+ })
})
diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.tsx
index 11ae2c194db61..b472551c66f0b 100644
--- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/PipelineCostDialog.tsx
@@ -27,9 +27,11 @@ import {
type ReplicationTableIdentity,
type TableSyncCopyConfig,
} from '@/components/interfaces/Database/Replication/TableSyncCopy.utils'
+import { InlineLink } from '@/components/ui/InlineLink'
import { useReplicationCostEstimateQuery } from '@/data/replication/cost-estimate-query'
import { useReplicationSourceId } from '@/data/replication/sources-query'
import { useLatest } from '@/hooks/misc/useLatest'
+import { DOCS_URL } from '@/lib/constants'
import { formatBytes, formatCurrency } from '@/lib/helpers'
const MAX_VISIBLE_TABLES = 10
@@ -138,11 +140,15 @@ export const PipelineCostDialog = ({
Initial sync
+
+ Quick planning estimate using available source table information. Final usage is
+ measured from the data successfully processed during initial sync.
+
{copyTableCount === 0 ? (
- No tables will be initially copied. All publication tables will still stream
- new changes, with no initial-copy charge.
+ No tables will run an initial sync. Ongoing replication will still process new
+ changes for every publication table, with no initial sync charge.
- An initial-copy estimate is unavailable for one or more selected tables. You
+ An initial sync estimate is unavailable for one or more selected tables. You
can still create the pipeline.
- Ongoing replication data
+
+ Ongoing replication data processed
+
{formatCurrency(estimate.streaming.rate_per_gb)}/GB
- Ongoing replication is billed on the volume of changes processed after the
- initial sync, so the total depends on how often your data changes.
+ Ongoing replication is billed on Postgres row data accepted by the destination,
+ so the total depends on how often your published data changes.
Destination-provider charges, such as BigQuery ingestion, storage, and compute,
- are separate.
+ are separate. See{' '}
+
+ how data processed is measured
+
+ .