feat(replication): Add new form for early access of replication destination (#47046)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added pre-release pages for Supabase Pipelines’ new destinations,
including early-access signup, thank-you confirmation, and related
resources.
  * Added destination options for ClickHouse, Snowflake, and DuckLake.
* **Bug Fixes**
  * Improved form validation for grouped required checkboxes.
  * Added clearer checkbox labels and descriptions.
* Forms now explain which required option groups still need a selection.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ana <30495040+ana1337x@users.noreply.github.com>
This commit is contained in:
Riccardo BusettiandAna authored and GitHub committed 2026-07-11 00:32:14 +02:00
1 parent a881220335
commit 8f82861627
5 files changed
+250 -3

No files matched your search

+4
View File
@@ -30,12 +30,16 @@ import amoe from './legal/amoe'
import amoeThankYou from './legal/amoe-thankyou'
import contestRules from './legal/contest-rules'
import byocEarlyAccess from './pre-release/byoc-early-access'
import supabasePipelinesNewDestinations from './pre-release/supabase-pipelines-new-destinations'
import supabasePipelinesNewDestinationsThankYou from './pre-release/supabase-pipelines-new-destinations-thank-you'
import supabaseStripeProjects from './stripe-projects/supabase-stripe-projects'
const pages: GoPageInput[] = [
awsActivateOffer, // maintain forever
exampleLeadGen, // sample lead gen page
byocEarlyAccess, // maintain until PM says to remove
supabasePipelinesNewDestinations, // maintain while Pipelines destination early access is active
supabasePipelinesNewDestinationsThankYou, // maintain while Pipelines destination early access is active
amoe, // maintain forever
amoeThankYou, // maintain forever
contestRules, // maintain forever
@@ -0,0 +1,26 @@
import type { GoPageInput } from 'marketing'
const page: GoPageInput = {
template: 'thank-you',
slug: 'supabase-pipelines-new-destinations/thank-you',
metadata: {
title: 'Supabase Pipelines destination request received',
description: 'Thanks for requesting early access to upcoming Supabase Pipelines destinations.',
ogImage: '/images/blog/2025-12-02-introducing-supabase-pipelines/og.png',
},
hero: {
title: 'Thanks for your request',
subtitle: 'Early Access',
description:
"We received your destination preferences for Supabase Pipelines. If your workspace is selected for early access to ClickHouse, Snowflake, or DuckLake destinations, we'll reach out with next steps.",
ctas: [
{
label: 'Back to Supabase',
href: '/',
variant: 'secondary',
},
],
},
}
export default page
@@ -0,0 +1,181 @@
import type { GoPageInput } from 'marketing'
const page: GoPageInput = {
template: 'lead-gen',
slug: 'supabase-pipelines-new-destinations',
metadata: {
title: 'Supabase Pipelines Destinations - Early Access',
description:
'Request early access to upcoming Supabase Pipelines destinations for ClickHouse, Snowflake, and DuckLake.',
ogImage: '/images/blog/2025-12-02-introducing-supabase-pipelines/og.png',
},
hero: {
title: 'Supabase Pipelines destinations',
subtitle: 'Early Access',
description:
"Managed change-data-capture pipelines that replicate your Supabase Postgres data to analytical systems in near real time. Request early access to the destinations we're adding next: ClickHouse, Snowflake, and DuckLake.",
image: {
src: '/images/blog/2025-12-02-introducing-supabase-pipelines/thumb.png',
alt: 'Introducing Supabase Pipelines',
width: 2400,
height: 1260,
},
ctas: [
{
label: 'Request early access',
href: '#form',
variant: 'primary',
},
{
label: 'Read the announcement',
href: '/blog/introducing-supabase-pipelines',
variant: 'secondary',
},
],
},
sections: [
{
type: 'feature-grid',
className: 'border-y border-muted bg-surface-75 py-16 sm:py-24',
title: 'Bring Supabase Postgres into your analytics stack',
description:
'Pipelines keeps your application workload on Postgres while streaming production changes into systems built for analytics, reporting, and lakehouse workflows.',
columns: 3,
items: [
{
title: 'ClickHouse',
description:
'Stream operational data into ClickHouse for high-volume analytical queries, dashboards, and event workloads.',
},
{
title: 'Snowflake',
description:
'Replicate Supabase Postgres data into Snowflake for warehouse analytics, BI, and governed data workflows.',
},
{
title: 'DuckLake',
description:
'Move data into an open lakehouse destination for portable analytics and long-term data workflows.',
},
],
},
{
type: 'form',
id: 'form',
className: 'py-4 sm:py-8',
title: 'Request early access to new Pipelines destinations',
description:
"This form is for early access to upcoming destinations for Supabase Pipelines, not access to Supabase Pipelines itself. Tell us which destinations your team wants to use. If your workspace is selected, we'll reach out with next steps.",
fields: [
{
type: 'email',
name: 'email',
label: 'Email address',
placeholder: 'Work email',
required: true,
},
{
type: 'text',
name: 'org_slug',
label: 'Supabase Organization Slug',
placeholder: 'acme-inc',
description: 'Use the slug from your Supabase Dashboard URL, for example /org/acme-inc.',
required: true,
},
{
type: 'checkbox',
name: 'destination_clickhouse',
label: 'ClickHouse',
description:
'For high-throughput analytical queries, event data, logs, and dashboard workloads.',
group: 'requested_destinations',
groupRequired: true,
},
{
type: 'checkbox',
name: 'destination_snowflake',
label: 'Snowflake',
description: 'For teams running warehouse analytics, BI, or governed data workflows.',
group: 'requested_destinations',
groupRequired: true,
},
{
type: 'checkbox',
name: 'destination_ducklake',
label: 'DuckLake',
description:
'For open lakehouse workflows where you want portable data and flexible analytics.',
group: 'requested_destinations',
groupRequired: true,
},
],
submitLabel: 'Request destination access',
successRedirect: '/go/supabase-pipelines-new-destinations/thank-you',
disclaimer:
'By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy).',
crm: {
notion: {
database_id: '3825004b775f80828179e7bdd7ee4b8d',
columnMap: {
email: 'Email',
org_slug: 'Organization Slug',
destination_clickhouse: 'ClickHouse',
destination_snowflake: 'Snowflake',
destination_ducklake: 'DuckLake',
},
},
},
},
{
type: 'three-column',
className: 'pb-16 sm:pb-24',
title: 'Suggested reading',
description: 'Get the product context and setup details behind Supabase Pipelines.',
children: (
<>
<a
href="/blog/introducing-supabase-pipelines"
className="group flex h-full flex-col gap-3 rounded-lg border border-muted bg-surface-100 p-5 text-left transition-colors hover:border-brand-600 hover:bg-surface-200"
>
<span className="text-sm text-foreground-lighter">Blog</span>
<span className="text-lg font-medium text-foreground group-hover:text-brand-link">
Introducing Supabase Pipelines
</span>
<span className="text-sm leading-relaxed text-foreground-light">
Learn how managed CDC pipelines move Postgres changes to analytical destinations in
near real time.
</span>
</a>
<a
href="/docs/guides/database/replication/pipelines"
className="group flex h-full flex-col gap-3 rounded-lg border border-muted bg-surface-100 p-5 text-left transition-colors hover:border-brand-600 hover:bg-surface-200"
>
<span className="text-sm text-foreground-lighter">Docs</span>
<span className="text-lg font-medium text-foreground group-hover:text-brand-link">
Set up Pipelines
</span>
<span className="text-sm leading-relaxed text-foreground-light">
Review publications, destination setup, monitoring, and how pipelines are managed from
the Dashboard.
</span>
</a>
<a
href="/blog/realtime-or-pipelines-how-to-choose-the-right-tool"
className="group flex h-full flex-col gap-3 rounded-lg border border-muted bg-surface-100 p-5 text-left transition-colors hover:border-brand-600 hover:bg-surface-200"
>
<span className="text-sm text-foreground-lighter">Guide</span>
<span className="text-lg font-medium text-foreground group-hover:text-brand-link">
Realtime or Pipelines?
</span>
<span className="text-sm leading-relaxed text-foreground-light">
Compare live user experiences over WebSocket with reliable data movement into
analytical systems.
</span>
</a>
</>
),
},
],
}
export default page
+32 -3
View File
@@ -149,13 +149,20 @@ function Field({
}) {
if (field.type === 'checkbox') {
return (
<label className="flex items-start gap-3 cursor-pointer text-sm text-foreground-light leading-relaxed">
<label className="flex items-start gap-3 cursor-pointer rounded-lg border border-muted bg-surface-100 p-4 text-sm text-foreground-light leading-relaxed transition-colors hover:bg-surface-200">
<Checkbox
className="mt-0.5"
checked={value === 'true'}
onCheckedChange={(checked) => onChange(checked === true ? 'true' : 'false')}
/>
<span>{field.label}</span>
<span className="flex flex-col gap-1">
<span className="font-medium text-foreground">{field.label}</span>
{field.description && (
<span className="text-xs text-foreground-lighter leading-relaxed">
{field.description}
</span>
)}
</span>
</label>
)
}
@@ -208,7 +215,7 @@ export default function MarketingForm({
// Required checkboxes aren't covered by HTML5 validation; check them manually.
const uncheckedRequired = visibleFields.filter(
(f) => f.type === 'checkbox' && f.required && values[f.name] !== 'true'
(f) => f.type === 'checkbox' && f.required && !f.group && values[f.name] !== 'true'
)
if (uncheckedRequired.length > 0) {
setSubmitState('error')
@@ -218,6 +225,28 @@ export default function MarketingForm({
return
}
const requiredCheckboxGroups = new Map<string, MarketingFormField[]>()
visibleFields.forEach((field) => {
if (field.type !== 'checkbox' || !field.group || !field.groupRequired) return
const groupFields = requiredCheckboxGroups.get(field.group) ?? []
groupFields.push(field)
requiredCheckboxGroups.set(field.group, groupFields)
})
const missingRequiredGroups = Array.from(requiredCheckboxGroups.values()).filter((group) =>
group.every((field) => values[field.name] !== 'true')
)
if (missingRequiredGroups.length > 0) {
setSubmitState('error')
setErrorMessages(
missingRequiredGroups.map((group) => {
const labels = group.map((field) => field.label).join(', ')
return `Please select at least one option: ${labels}`
})
)
return
}
// Strip values for fields that are currently hidden so stale data doesn't leak.
const submittedValues = Object.fromEntries(
Object.entries(values).filter(([name]) => visibleFieldNames.has(name))
+7
View File
@@ -166,6 +166,13 @@ export const selectFieldSchema = formFieldBase.extend({
export const checkboxFieldSchema = formFieldBase.extend({
type: z.literal('checkbox'),
/**
* Optional group identifier for related checkboxes. When any visible checkbox
* in a group has `groupRequired: true`, at least one checkbox in that group
* must be selected.
*/
group: z.string().optional(),
groupRequired: z.boolean().optional().default(false),
})
export const formFieldSchema = z.discriminatedUnion('type', [