Skip to content
Merged
3 changes: 2 additions & 1 deletion apps/docs/content/guides/deployment/branching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Supabase branches create separate environments that spin off from your main proj
- **Preview Branches**: Preview branches are ephemeral and best suited for focused testing. They are automatically deleted when a PR is merged or closed.
- **Persistent Branches**: Persistent branches are long-lived and recommended for environments like staging, QA, or development. They aren't automatically paused or deleted due to inactivity or when a PR is merged or closed.
- **Managing Branches**: You can create, review, and merge branches either automatically via our [GitHub integration](/docs/guides/deployment/branching/github-integration) or directly [through the dashboard](/docs/guides/deployment/branching/dashboard) (currently in beta). All branches show up in the branches page in the dashboard, regardless of how they were created.
- **Data-less**: New branches do not start with any data from your main project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration.
- **Cloned from your main project**: Each new branch is created as a clone of your main project. It starts with that project's Edge Functions deployed and the configuration set.
- **Data-less by default**: By default, new branches do not start with any data or storage objects from your main project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration, or the [Include data](/docs/guides/deployment/branching/dashboard#include-production-data) option if you create the branch from the dashboard.

## Deploying to production

Expand Down
18 changes: 18 additions & 0 deletions apps/docs/content/guides/deployment/branching/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ Once you've enabled the feature, you can create a new branch:
1. Click the arrows next to the branch name in the top menu bar. (The top menu bar has the format `YOUR_ORGANIZATION / YOUR_PROJECT / CURRENT_BRANCH_NAME`.)
2. Click `Create branch`.

The new branch is a clone of your base project. It starts with the project's schema, Edge Functions, and the configuration set.

### Include production data

By default, a branch starts without any of your production data or storage objects. If your project has the Point-in-Time Recovery add-on, you can turn on **Include data** when you create the branch to copy your production data into it.

<Admonition type="caution">

A branch created with **Include data** holds a copy of your production data, treat it with the same care as production.

</Admonition>

<Admonition type="note">

A branch uses a larger disk and matches the compute size of your project, which increases its cost.

</Admonition>

## Making changes to a branch

Use the branch selector in the top bar to change to your branch. Any changes you make (including SQL run in the SQL editor, table editor changes, and configuration changes) are now made against the currently selected branch.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,25 @@ Enable the **Automatic branching** option in your GitHub Integration configurati

When a new branch is created in GitHub, a corresponding branch is created in Supabase. (You can enable the **Supabase changes only** option to only create Supabase branches when Supabase files change.)

Every Supabase branch, preview or persistent, is created as a clone of your base project. The new branch starts with the Edge Functions and configuration of that project. Its database schema is not cloned. Instead, it is built from the migrations you commit to your repository.

### Configuration

You can test configuration changes on your Preview Branch by configuring the `config.toml` file in your Supabase directory. See the [Configuration docs](/docs/guides/deployment/branching/configuration) for more information.

Your branch starts with the configuration of your base project. The settings in your `config.toml` file are applied on top of that clone.

A comment is added to your PR with the deployment status of your preview branch.

### Migrations

The migrations in the `migrations` subdirectory of your Supabase directory are automatically run.
The migrations in the `migrations` subdirectory of your Supabase directory are automatically run when the branch is created. Each later commit runs only the migrations that haven't been applied yet.

If you want to rerun existing migrations, reset the branch from the Supabase dashboard to start from scratch. Note that existing data on your branch will also be dropped by a reset.

### Seeding

No production data is copied to your Preview branch. This is meant to protect your sensitive production data.
Cloning your base project copies its Edge Functions and configuration, but not its data or storage objects. This is meant to protect your sensitive production data. Your branch starts with the tables your migrations create, and the only rows in them are the ones your seed files add.

You can seed your Preview Branch with sample data using the `seed.sql` file in your Supabase directory. See the [Seeding docs](/docs/guides/local-development/seeding-your-database) for more information.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ You might want to roll back changes you've made in an earlier migration change.

To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it.

The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order.
The new preview branch is a fresh clone of your base project and is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost.

To rerun migrations that your base project has already applied, reset the branch from the Supabase dashboard instead. A reset reruns all migrations in sequential order and drops existing data on the branch.

### Deployment failures

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ After completing the steps above, you should receive a Slack message whenever an

Migrations are run in sequential order. Each migration builds upon the previous one.

The preview branch has a record of which migrations have been applied, and only applies new migrations for each commit. This can create an issue when rolling back migrations.
The preview branch inherits the migration history of your base project, so it only applies migrations that haven't been run yet. This can create an issue when rolling back migrations.

### Using ORM or custom seed scripts

Expand Down Expand Up @@ -237,7 +237,9 @@ You might want to roll back changes you've made in an earlier migration change.

To fix this, push the latest changes, then delete the preview branch in Supabase and reopen it.

The new preview branch is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. This is equivalent to running `supabase db reset` locally. All migrations are rerun in sequential order.
The new preview branch is a fresh clone of your base project and is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost.

To rerun migrations that your base project has already applied, reset the branch from the Supabase dashboard instead. A reset reruns all migrations in sequential order and drops existing data on the branch.

### Seeding behavior

Expand Down
1 change: 1 addition & 0 deletions apps/studio/TANSTACK_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
### Project shell — `/workers/*`

- [x] A `routes/project/$ref/workers/index.tsx` ← `pages/project/[ref]/workers/index.tsx`
- [x] A `routes/project/$ref/workers/$name.tsx` ← `pages/project/[ref]/workers/[name].tsx`

### Project shell — `/functions/*`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT =
'Purchase IPv4 add-on or use Shared Pooler if on a IPv4 network'
export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on'

export const CONNECTION_SOURCE_LOAD_BALANCER = 'load-balancer'

export type ConnectionStringMethod = 'direct' | 'transaction' | 'session'

export const connectionStringMethodOptions: Record<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,14 @@ export function ConnectConfigSection({
layout="horizontal"
label={field.label}
description={field.description}
name={`connect-${field.id}`}
>
<Select
value={String(value ?? '')}
onValueChange={(v) => onFieldChange(field.id, v)}
>
<SelectTrigger
id={`connect-${field.id}`}
size="small"
className="[&>span:first-child]:flex [&>span:first-child]:items-center [&>span:first-child]:gap-x-2"
>
Expand Down Expand Up @@ -167,6 +169,7 @@ export function ConnectConfigSection({
layout="horizontal"
label={field.label}
description={field.description}
name={field.id}
className="[&>div>label>span]:break-keep! [&>div>label>span]:text-balance"
>
<Switch
Expand All @@ -185,12 +188,14 @@ export function ConnectConfigSection({
layout="horizontal"
label={field.label}
description={field.description}
name={`connect-${field.id}`}
>
<MultiSelector
values={Array.isArray(value) ? value : []}
onValuesChange={(v) => onFieldChange(field.id, v)}
>
<MultiSelectorTrigger
id={`connect-${field.id}`}
className="w-full"
label="Select features"
badgeLimit="wrap"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsS
const { ref } = useParams()
const stepsContainerRef = useRef<HTMLDivElement | null>(null)
const deploymentMode = useDeploymentMode()
const isHighAvailability = useIsHighAvailability()
const connectionStringPooler = useConnectionStringPooler(deploymentMode)

const { data: ipv4Addon } = useProjectAddonsQuery(
Expand All @@ -227,6 +228,7 @@ export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsS
connectionMethod: state.connectionMethod,
useSharedPooler: state.useSharedPooler,
hasIpv4Addon: !!ipv4Addon,
isHighAvailability,
})
const showSessionPoolerNotice = shouldShowSessionPoolerNotice({
isPlatform: deploymentMode.isPlatform,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ export function shouldShowIpv4AddonNotice({
connectionMethod,
useSharedPooler,
hasIpv4Addon,
isHighAvailability,
}: {
isPlatform: boolean
mode: ConnectMode
connectionMethod: FieldValue
useSharedPooler: FieldValue
hasIpv4Addon: boolean
isHighAvailability: boolean
}): boolean {
if (!isPlatform || mode !== 'direct' || hasIpv4Addon) return false
// The IPv4 add-on does not apply to Multigres connections
if (!isPlatform || mode !== 'direct' || hasIpv4Addon || isHighAvailability) return false
return connectionMethod === 'direct' || (connectionMethod === 'transaction' && !useSharedPooler)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@ export const buildJdbcString = (params: ConnectionParams) => {
return `jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${PASSWORD_PLACEHOLDER}${extraParams}`
}

/**
* Ensures a connection string's query params carry `sslmode=require` without
* dropping params the URI already has (e.g. `options=reference%3D...` or
* `sslnegotiation=direct`).
*/
export const withRequiredSslmode = (search: string) => {
if (!search) return '?sslmode=require'
if (search.includes('sslmode=')) return search
return `${search}&sslmode=require`
}

export const buildDotnetConnectionString = (params: ConnectionParams) => {
// Multigres only accepts direct SSL negotiation; Npgsql (9+) spells it
// `SSL Negotiation=Direct` and throws on the parameter in older versions,
// so only emit it when the resolved URI carries the param.
const sslNegotiation = params.search.includes('sslnegotiation=direct')
? ';SSL Negotiation=Direct'
: ''
return `Host=${params.host};Port=${params.port};Database=${params.database};Username=${params.user};Password=${PASSWORD_PLACEHOLDER};SSL Mode=Require;Trust Server Certificate=true${sslNegotiation}`
}

export const buildConnectionStringWithPassword = (
connectionString: string,
password: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ export const appendHighAvailabilitySslParams = (uri: string) =>
? uri
: appendConnectionStringParams(uri, HIGH_AVAILABILITY_SSL_PARAMS)

/**
* The Multigres read-only load balancer listens on this port on the same host
* as the primary database.
*/
export const HIGH_AVAILABILITY_LOAD_BALANCER_PORT = 5433

export const getHighAvailabilityLoadBalancerConnectionInfo = <
T extends { db_port: number | string },
>(
connectionInfo: T
): T => ({ ...connectionInfo, db_port: HIGH_AVAILABILITY_LOAD_BALANCER_PORT })

type ConnectionStrings = {
psql: string
uri: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func main() {
},
dotnet: {
installCommands: [
// SSL Negotiation=Direct in the generated connection string requires Npgsql 9.0+.
// Concrete version: quoting a floating version breaks on Windows cmd, and
// floating versions fail under Central Package Management (NU1011).
'dotnet add package Npgsql --version 9.0.5',
'dotnet add package Microsoft.Extensions.Configuration.Json --version YOUR_DOTNET_VERSION',
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ describe('shouldShowIpv4AddonNotice', () => {
connectionMethod: 'direct',
useSharedPooler: false,
hasIpv4Addon: false,
isHighAvailability: false,
}

test('returns true for a direct connection with no IPv4 addon', () => {
Expand Down Expand Up @@ -169,6 +170,10 @@ describe('shouldShowIpv4AddonNotice', () => {
test('returns false when self-hosted (not platform)', () => {
expect(shouldShowIpv4AddonNotice({ ...BASE, isPlatform: false })).toBe(false)
})

test('returns false for high-availability projects even with a direct connection and no addon', () => {
expect(shouldShowIpv4AddonNotice({ ...BASE, isHighAvailability: true })).toBe(false)
})
})

describe('shouldShowSessionPoolerNotice', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
appendConnectionStringParams,
buildConnectionParameters,
buildConnectionStringWithPassword,
buildDotnetConnectionString,
buildJdbcString,
buildPsqlCommand,
buildSafeConnectionString,
DEFAULT_PORT,
parseConnectionParams,
PASSWORD_PLACEHOLDER,
resolveConnectionString,
withRequiredSslmode,
} from '../ConnectionString.utils'

describe('parseConnectionParams', () => {
Expand Down Expand Up @@ -236,6 +238,52 @@ describe('buildJdbcString', () => {
})
})

describe('withRequiredSslmode', () => {
test('returns ?sslmode=require for an empty search', () => {
expect(withRequiredSslmode('')).toBe('?sslmode=require')
})

test('leaves a search that already sets sslmode unchanged', () => {
expect(withRequiredSslmode('?sslmode=require&sslnegotiation=direct')).toBe(
'?sslmode=require&sslnegotiation=direct'
)
})

test('appends sslmode=require to existing params without it', () => {
expect(withRequiredSslmode('?options=reference%3Dproj')).toBe(
'?options=reference%3Dproj&sslmode=require'
)
})
})

describe('buildDotnetConnectionString', () => {
const params = {
host: 'db.proj.supabase.co',
port: '5432',
user: 'postgres',
database: 'postgres',
search: '',
}

test('builds the base Npgsql string without SSL negotiation', () => {
expect(buildDotnetConnectionString(params)).toBe(
`Host=db.proj.supabase.co;Port=5432;Database=postgres;Username=postgres;Password=${PASSWORD_PLACEHOLDER};SSL Mode=Require;Trust Server Certificate=true`
)
})

test('appends SSL Negotiation=Direct when the URI requires direct negotiation', () => {
expect(
buildDotnetConnectionString({
...params,
port: '5433',
search: '?sslmode=require&sslnegotiation=direct',
})
).toBe(
`Host=db.proj.supabase.co;Port=5433;Database=postgres;Username=postgres;Password=${PASSWORD_PLACEHOLDER};SSL Mode=Require;Trust Server Certificate=true;SSL Negotiation=Direct`
)
})
})

describe('buildConnectionParameters', () => {
test('produces host/port/database/user rows in display order', () => {
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { describe, expect, test } from 'vitest'
import {
appendHighAvailabilitySslParams,
buildConnectionStringPooler,
getConnectionStrings,
getHighAvailabilityLoadBalancerConnectionInfo,
getSelfHostedDirectStrings,
getSelfHostedPoolerStrings,
HIGH_AVAILABILITY_LOAD_BALANCER_PORT,
HIGH_AVAILABILITY_SSL_PARAMS,
} from '../DatabaseSettings.utils'
import type { DeploymentMode } from '@/hooks/misc/useDeploymentMode'
Expand Down Expand Up @@ -239,3 +242,45 @@ describe('appendHighAvailabilitySslParams', () => {
expect(appendHighAvailabilitySslParams('')).toBe('')
})
})

describe('getHighAvailabilityLoadBalancerConnectionInfo', () => {
test('swaps the port for the load balancer port and preserves the other fields', () => {
const connectionInfo = {
db_user: 'postgres',
db_port: 5432,
db_host: 'db.proj.supabase.co',
db_name: 'postgres',
}

expect(getHighAvailabilityLoadBalancerConnectionInfo(connectionInfo)).toEqual({
db_user: 'postgres',
db_port: HIGH_AVAILABILITY_LOAD_BALANCER_PORT,
db_host: 'db.proj.supabase.co',
db_name: 'postgres',
})
})

test('builds a read-only load balancer connection string on port 5433 with SSL params', () => {
const loadBalancerInfo = getHighAvailabilityLoadBalancerConnectionInfo({
db_user: 'postgres',
db_port: 5432,
db_host: 'db.proj.supabase.co',
db_name: 'postgres',
})

const result = buildConnectionStringPooler({
deploymentMode: platform,
connectionInfo: loadBalancerInfo,
connectionStringsShared: getConnectionStrings({
connectionInfo: loadBalancerInfo,
metadata: { projectRef: 'proj' },
}),
ipv4Addon: false,
isHighAvailability: true,
})

expect(result.direct).toBe(
`postgresql://postgres:[YOUR-PASSWORD]@db.proj.supabase.co:5433/postgres?${HIGH_AVAILABILITY_SSL_PARAMS}`
)
})
})
Loading
Loading