-
- Sync tooltips
-
-
-
- When enabled, also shows tooltips on all charts.{' '}
-
- Requires header sync.
-
+
+
+
+
+ Sync chart headers
+
+ Hovering over any chart will update headers across all charts
+
-
-
+
+ {syncHover && (
+
+
+ Sync tooltips
+
+ Shows tooltips on all charts
+
+
+
+
+ )}
+
+
)
}
diff --git a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx
index b07542f36b5..69bf1912450 100644
--- a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx
+++ b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx
@@ -111,7 +111,7 @@ export function DataTableFilterCheckbox({
/>
{Component ? (
@@ -126,7 +126,11 @@ export function DataTableFilterCheckbox
({
aria-label={isExpanded ? 'Collapse' : 'Expand'}
aria-expanded={isExpanded}
onClick={() => toggleExpanded(optionKey)}
- className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-foreground-lighter hover:bg-selection hover:text-foreground mr-2.5"
+ className={cn(
+ 'flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-foreground-lighter',
+ 'hover:bg-selection hover:text-foreground',
+ 'absolute top-0 right-10'
+ )}
>
{isExpanded ? : }
@@ -148,15 +152,17 @@ export function DataTableFilterCheckbox({
'rounded-md ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
)}
>
- only
+ only
+
{hasNested &&
isExpanded &&
option.options?.map((optionNested, nestedIndex) => {
const nestedChecked = getBooleanParam(optionNested.value)
const isLastNested = nestedIndex === (option.options?.length ?? 0) - 1
+
return (
({
mutationFn: (vars) => deleteBucket(vars),
async onSuccess(data, variables, context) {
- const { projectRef } = variables
- await queryClient.invalidateQueries({ queryKey: storageKeys.buckets(projectRef) })
+ const { projectRef, id } = variables
+
+ const deletedBucketQueryKey = storageKeys.bucket(projectRef, id)
+ await queryClient.cancelQueries({ queryKey: deletedBucketQueryKey })
+ queryClient.removeQueries({ queryKey: deletedBucketQueryKey })
+
await onSuccess?.(data, variables, context)
+
+ // Fire-and-forget: only the bucket list needs refreshing, and it shouldn't block
+ // onSuccess (modal close/navigation) above.
+ void queryClient.invalidateQueries({ queryKey: storageKeys.bucketsList(projectRef) })
},
async onError(data, variables, context) {
if (onError === undefined) {
diff --git a/apps/studio/data/storage/buckets-query.ts b/apps/studio/data/storage/buckets-query.ts
index f566d1b7445..ab9896e98e0 100644
--- a/apps/studio/data/storage/buckets-query.ts
+++ b/apps/studio/data/storage/buckets-query.ts
@@ -227,6 +227,11 @@ export const useBucketInfoQueryPreferCached = (bucketId?: string, projectRef?: s
const shouldRetryBucketsQuery = (failureCount: number, error: unknown) => {
if (error instanceof ResponseError) {
+ // If the bucket doesn't exist or was deleted, don't retry — it will never succeed.
+ if (error.code === 404) {
+ return false
+ }
+
if (
error.message.includes('Missing tenant config') ||
error.message.includes('Project has no active API keys')
diff --git a/apps/studio/fonts/CustomFont-Black.woff2 b/apps/studio/fonts/CustomFont-Black.woff2
deleted file mode 100644
index e3c834e57f2..00000000000
Binary files a/apps/studio/fonts/CustomFont-Black.woff2 and /dev/null differ
diff --git a/apps/studio/fonts/CustomFont-BlackItalic.woff2 b/apps/studio/fonts/CustomFont-BlackItalic.woff2
deleted file mode 100644
index f84036283ec..00000000000
Binary files a/apps/studio/fonts/CustomFont-BlackItalic.woff2 and /dev/null differ
diff --git a/apps/studio/fonts/CustomFont-Bold.woff2 b/apps/studio/fonts/CustomFont-Bold.woff2
deleted file mode 100644
index 5e7af459481..00000000000
Binary files a/apps/studio/fonts/CustomFont-Bold.woff2 and /dev/null differ
diff --git a/apps/studio/fonts/CustomFont-BoldItalic.woff2 b/apps/studio/fonts/CustomFont-BoldItalic.woff2
deleted file mode 100644
index ac0edd55fd3..00000000000
Binary files a/apps/studio/fonts/CustomFont-BoldItalic.woff2 and /dev/null differ
diff --git a/apps/studio/fonts/CustomFont-Book.woff2 b/apps/studio/fonts/CustomFont-Book.woff2
deleted file mode 100644
index abd31f7ec27..00000000000
Binary files a/apps/studio/fonts/CustomFont-Book.woff2 and /dev/null differ
diff --git a/apps/studio/fonts/CustomFont-BookItalic.woff2 b/apps/studio/fonts/CustomFont-BookItalic.woff2
deleted file mode 100644
index d326c8672cc..00000000000
Binary files a/apps/studio/fonts/CustomFont-BookItalic.woff2 and /dev/null differ
diff --git a/apps/studio/fonts/CustomFont-Medium.woff2 b/apps/studio/fonts/CustomFont-Medium.woff2
deleted file mode 100644
index c07131ddec9..00000000000
Binary files a/apps/studio/fonts/CustomFont-Medium.woff2 and /dev/null differ
diff --git a/apps/studio/fonts/index.ts b/apps/studio/fonts/index.ts
index 272cd5acd2d..431e1d45203 100644
--- a/apps/studio/fonts/index.ts
+++ b/apps/studio/fonts/index.ts
@@ -1,53 +1,44 @@
-import { Source_Code_Pro } from 'next/font/google'
+import { Manrope } from 'next/font/google'
import localFont from 'next/font/local'
-export const customFont = localFont({
- variable: '--font-custom',
+export const manrope = Manrope({
+ variable: '--font-manrope',
display: 'swap',
- fallback: ['Circular', 'custom-font', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
+ subsets: ['latin'],
+})
+
+export const inter = localFont({
+ variable: '--font-inter',
+ display: 'swap',
+ fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
src: [
{
- path: './CustomFont-Book.woff2',
- weight: '400',
+ path: './inter/InterVariable.woff2',
+ weight: '100 900',
style: 'normal',
},
{
- path: './CustomFont-BookItalic.woff2',
- weight: '400',
- style: 'italic',
- },
- {
- path: './CustomFont-Medium.woff2',
- weight: '500',
- style: 'normal',
- },
- {
- path: './CustomFont-Bold.woff2',
- weight: '700',
- style: 'normal',
- },
- {
- path: './CustomFont-BoldItalic.woff2',
- weight: '700',
- style: 'italic',
- },
- {
- path: './CustomFont-Black.woff2',
- weight: '800',
- style: 'normal',
- },
- {
- path: './CustomFont-BlackItalic.woff2',
- weight: '800',
+ path: './inter/InterVariable-Italic.woff2',
+ weight: '100 900',
style: 'italic',
},
],
})
-export const sourceCodePro = Source_Code_Pro({
- subsets: ['latin'],
- fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'],
+export const sourceCodePro = localFont({
variable: '--font-source-code-pro',
display: 'swap',
- weight: ['400', '500', '600', '700'],
+ fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'],
+ src: [
+ {
+ path: './source-code-pro/SourceCodePro-Variable.woff2',
+ weight: '200 900',
+ style: 'normal',
+ },
+ {
+ path: './source-code-pro/SourceCodePro-Variable-Italic.woff2',
+ weight: '200 900',
+ style: 'italic',
+ },
+ ],
})
diff --git a/apps/studio/fonts/inter/InterVariable-Italic.woff2 b/apps/studio/fonts/inter/InterVariable-Italic.woff2
new file mode 100644
index 00000000000..2f06c65192b
Binary files /dev/null and b/apps/studio/fonts/inter/InterVariable-Italic.woff2 differ
diff --git a/apps/studio/fonts/inter/InterVariable.woff2 b/apps/studio/fonts/inter/InterVariable.woff2
new file mode 100644
index 00000000000..d15208de03c
Binary files /dev/null and b/apps/studio/fonts/inter/InterVariable.woff2 differ
diff --git a/apps/studio/fonts/source-code-pro/SourceCodePro-Variable-Italic.woff2 b/apps/studio/fonts/source-code-pro/SourceCodePro-Variable-Italic.woff2
new file mode 100644
index 00000000000..893f998497c
Binary files /dev/null and b/apps/studio/fonts/source-code-pro/SourceCodePro-Variable-Italic.woff2 differ
diff --git a/apps/studio/fonts/source-code-pro/SourceCodePro-Variable.woff2 b/apps/studio/fonts/source-code-pro/SourceCodePro-Variable.woff2
new file mode 100644
index 00000000000..bc303f50c5d
Binary files /dev/null and b/apps/studio/fonts/source-code-pro/SourceCodePro-Variable.woff2 differ
diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx
index 1c1342f96b8..2a4421b4a1d 100644
--- a/apps/studio/pages/_app.tsx
+++ b/apps/studio/pages/_app.tsx
@@ -52,7 +52,7 @@ import { GlobalErrorBoundaryState } from '@/components/ui/ErrorBoundary/GlobalEr
import { GlobalShortcuts } from '@/components/ui/GlobalShortcuts/GlobalShortcuts'
import { getCLIReleaseVersion } from '@/data/misc/cli-release-version-query'
import { useRootQueryClient } from '@/data/query-client'
-import { customFont, sourceCodePro } from '@/fonts'
+import { inter, manrope, sourceCodePro } from '@/fonts'
import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { AuthProvider } from '@/lib/auth'
@@ -186,7 +186,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
{/* [Alaister]: This has to be an inline style tag here and not a separate component due to next/font */}
{/* Speed up initial API loading times by pre-connecting to the API domain */}
diff --git a/apps/studio/pages/_document.tsx b/apps/studio/pages/_document.tsx
index b743dc5daef..8560136df0f 100644
--- a/apps/studio/pages/_document.tsx
+++ b/apps/studio/pages/_document.tsx
@@ -1,5 +1,7 @@
import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document'
+import { inter, manrope, sourceCodePro } from '@/fonts'
+
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx)
@@ -11,7 +13,7 @@ class MyDocument extends Document {
return (
-
+
diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx
index 93a0784e951..f4d2b74a564 100644
--- a/apps/studio/pages/new/[slug].tsx
+++ b/apps/studio/pages/new/[slug].tsx
@@ -107,7 +107,6 @@ const Wizard: NextPageWithLayout = () => {
)
const { hasLoaded: flagsLoaded } = useFeatureFlags()
- const smartRegionEnabled = useFlag('enableSmartRegion')
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const showInternalOnlyConfiguration = useFlag('newProjectInternalOnlyConfiguration')
@@ -172,6 +171,7 @@ const Wizard: NextPageWithLayout = () => {
} = useWatch({ control: form.control })
const { dirtyFields } = useFormState(form)
const isDbRegionDirty = dirtyFields.dbRegion
+ const smartRegionEnabled = cloudProvider !== 'AWS_NIMBUS'
// Read dirty state during render rather than depending on form.formState in the
// effect — form.formState is a Proxy that gets a new reference every render, which
@@ -221,9 +221,10 @@ const Wizard: NextPageWithLayout = () => {
? 0
: monthlyInstancePrice(instanceSize) - availableComputeCredits
+ const selectedCloudProvider = cloudProvider as CloudProvider
const { data: autoDefaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
{
- cloudProvider: PROVIDERS[defaultProvider].id,
+ cloudProvider: selectedCloudProvider,
},
{
enabled: flagsLoaded && !smartRegionEnabled,
@@ -255,7 +256,7 @@ const Wizard: NextPageWithLayout = () => {
? availableRegionsData?.recommendations.smartGroup.name
: ''
- const fixedDefaultRegion = PROVIDERS[defaultProvider].default_region.displayName
+ const fixedDefaultRegion = PROVIDERS[selectedCloudProvider].default_region.displayName
const regionError =
smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS'
? availableRegionsError
diff --git a/apps/studio/pages/project/[ref]/auth/providers.tsx b/apps/studio/pages/project/[ref]/auth/providers.tsx
index 5ddea6a4336..70d51d19b4d 100644
--- a/apps/studio/pages/project/[ref]/auth/providers.tsx
+++ b/apps/studio/pages/project/[ref]/auth/providers.tsx
@@ -1,4 +1,3 @@
-import { useFlag } from 'common'
import { PageContainer } from 'ui-patterns/PageContainer'
import { AuthProvidersForm } from '@/components/interfaces/Auth/AuthProvidersForm'
@@ -12,13 +11,12 @@ import type { NextPageWithLayout } from '@/types'
const ProvidersPage: NextPageWithLayout = () => {
const showProviders = useIsFeatureEnabled('authentication:show_providers')
const showCustomProviders = useIsFeatureEnabled('authentication:show_custom_providers')
- const isOauthProvidersEnabled = useFlag('CustomOauthProviders')
return (
{showProviders && }
- {showCustomProviders && isOauthProvidersEnabled && }
+ {showCustomProviders && }
)
}
diff --git a/apps/studio/pages/project/[ref]/database/policies.tsx b/apps/studio/pages/project/[ref]/database/policies.tsx
index fec4d768ee9..654a48de3fb 100644
--- a/apps/studio/pages/project/[ref]/database/policies.tsx
+++ b/apps/studio/pages/project/[ref]/database/policies.tsx
@@ -23,8 +23,8 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { useIsInlineEditorEnabled } from '@/components/interfaces/Account/Preferences/useDashboardSettings'
import { useIsRLSTesterEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { Policies } from '@/components/interfaces/Database/Policies/Policies'
+import { getGeneralPolicyTemplates } from '@/components/interfaces/Database/Policies/Policies.constants'
import { PoliciesDataProvider } from '@/components/interfaces/Database/Policies/PoliciesDataContext'
-import { getGeneralPolicyTemplates } from '@/components/interfaces/Database/Policies/PolicyEditorModal/PolicyEditorModal.constants'
import { PolicyEditorPanel } from '@/components/interfaces/Database/Policies/PolicyEditorPanel'
import {
generatePolicyUpdateSQL,
diff --git a/apps/studio/state/sql-editor/sql-editor-lifecycle.test.ts b/apps/studio/state/sql-editor/sql-editor-lifecycle.test.ts
index 45419f50b7b..6e6def1faf8 100644
--- a/apps/studio/state/sql-editor/sql-editor-lifecycle.test.ts
+++ b/apps/studio/state/sql-editor/sql-editor-lifecycle.test.ts
@@ -8,6 +8,7 @@ import {
isNewFolder,
isSaveFailed,
isSaving,
+ statusOnEdit,
statusOnSaveError,
statusOnSaveStart,
statusOnSaveSuccess,
@@ -108,6 +109,24 @@ describe('statusOnSaveError', () => {
})
})
+describe('statusOnEdit', () => {
+ it('marks a persisted, clean snippet as unsaved', () => {
+ expect(statusOnEdit('saved')).toBe('unsaved')
+ })
+
+ it('leaves every already-dirty or in-flight status unchanged', () => {
+ for (const status of [...NEVER_PERSISTED, 'unsaved', 'saving', 'save_failed'] as const) {
+ expect(statusOnEdit(status)).toBe(status)
+ }
+ })
+
+ it('keeps hasUnsavedChanges true after an edit', () => {
+ for (const status of [...NEVER_PERSISTED, ...PERSISTED] as const) {
+ expect(hasUnsavedChanges(statusOnEdit(status))).toBe(true)
+ }
+ })
+})
+
describe('lifecycle round trips', () => {
it('new snippet: first save succeeds then a re-save succeeds', () => {
let status: SnippetStatus = 'new'
diff --git a/apps/studio/state/sql-editor/sql-editor-lifecycle.ts b/apps/studio/state/sql-editor/sql-editor-lifecycle.ts
index cc3013aae78..190231b1fb7 100644
--- a/apps/studio/state/sql-editor/sql-editor-lifecycle.ts
+++ b/apps/studio/state/sql-editor/sql-editor-lifecycle.ts
@@ -48,6 +48,18 @@ export function statusOnSaveError(status: SnippetStatus | undefined): SnippetSta
return wasNeverPersisted(status) ? 'new_save_failed' : 'save_failed'
}
+/**
+ * Transition when the snippet is edited locally. A persisted-and-clean snippet
+ * becomes 'unsaved' so its dirty state is durable immediately (rather than only
+ * once the debounced save begins). Every other status is already dirty or
+ * in-flight — the never-persisted family, a save in progress, or a failed
+ * save — and is left unchanged so the persistence and progress axes are
+ * preserved.
+ */
+export function statusOnEdit(status: SnippetStatus): SnippetStatus {
+ return status === 'saved' ? 'unsaved' : status
+}
+
/**
* The lifecycle of a folder in the SQL editor nav, as a single set of
* mutually-exclusive states. Like SnippetStatus, this collapses two orthogonal
diff --git a/apps/studio/state/sql-editor/sql-editor-state.ts b/apps/studio/state/sql-editor/sql-editor-state.ts
index 56c527876a0..52e79e2252d 100644
--- a/apps/studio/state/sql-editor/sql-editor-state.ts
+++ b/apps/studio/state/sql-editor/sql-editor-state.ts
@@ -4,7 +4,7 @@ import { toast } from 'sonner'
import { proxy, snapshot, useSnapshot } from 'valtio'
import { devtools, proxyMap } from 'valtio/utils'
-import { folderStatusOnSaveStart, isNewFolder } from './sql-editor-lifecycle'
+import { folderStatusOnSaveStart, isNewFolder, statusOnEdit } from './sql-editor-lifecycle'
import { sqlEditorSessionState } from './sql-editor-session-state'
import type { StateSnippet, StateSnippetFolder } from './types'
import type { SnippetWithContent } from '@/data/content/sql-folders-query'
@@ -110,6 +110,10 @@ export const sqlEditorState = proxy({
let snippet = sqlEditorState.snippets[id]?.snippet
if (snippet?.content) {
snippet.content.unchecked_sql = untrustedSql(sql)
+ // Mark dirty immediately so `status` (not the transient needsSaving queue)
+ // is the durable source of truth for unsaved changes, even before the
+ // debounced save begins.
+ snippet.status = statusOnEdit(snippet.status)
sqlEditorState.needsSaving.set(id, shouldInvalidate)
}
},
diff --git a/apps/studio/styles/fonts.css b/apps/studio/styles/fonts.css
index 46905213121..e364ee2c426 100644
--- a/apps/studio/styles/fonts.css
+++ b/apps/studio/styles/fonts.css
@@ -7,62 +7,22 @@
*/
@font-face {
- font-family: 'CustomFont';
- src: url('../fonts/CustomFont-Book.woff2') format('woff2');
- font-weight: 400;
+ font-family: 'Inter';
+ src: url('../fonts/inter/InterVariable.woff2') format('woff2');
+ font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
- font-family: 'CustomFont';
- src: url('../fonts/CustomFont-BookItalic.woff2') format('woff2');
- font-weight: 400;
- font-style: italic;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'CustomFont';
- src: url('../fonts/CustomFont-Medium.woff2') format('woff2');
- font-weight: 500;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'CustomFont';
- src: url('../fonts/CustomFont-Bold.woff2') format('woff2');
- font-weight: 700;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'CustomFont';
- src: url('../fonts/CustomFont-BoldItalic.woff2') format('woff2');
- font-weight: 700;
- font-style: italic;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'CustomFont';
- src: url('../fonts/CustomFont-Black.woff2') format('woff2');
- font-weight: 800;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'CustomFont';
- src: url('../fonts/CustomFont-BlackItalic.woff2') format('woff2');
- font-weight: 800;
+ font-family: 'Inter';
+ src: url('../fonts/inter/InterVariable-Italic.woff2') format('woff2');
+ font-weight: 100 900;
font-style: italic;
font-display: swap;
}
:root {
- --font-custom: 'CustomFont', Circular, 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ --font-custom: 'Inter', system-ui, 'Helvetica Neue', Helvetica, Arial, sans-serif;
--font-source-code-pro: 'Source Code Pro', 'Office Code Pro', Menlo, monospace;
}
diff --git a/apps/studio/styles/globals.css b/apps/studio/styles/globals.css
index f312b19cfa8..1a6b3947c63 100644
--- a/apps/studio/styles/globals.css
+++ b/apps/studio/styles/globals.css
@@ -7,6 +7,10 @@
@source './../../../packages/ui/src/**/*.{tsx,ts,js}';
@source './../../../packages/ui-patterns/src/**/*.{tsx,ts,js}';
+@theme inline {
+ --font-heading: var(--font-heading, var(--font-sans));
+}
+
@theme {
--text-grid: 13px;
@@ -47,6 +51,21 @@
--animate-chevron-up: chevron-up 2s ease-in-out infinite;
--animate-sway: sway 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-marketplace-featured-progress: marketplace-featured-progress linear forwards;
+
+ /* font sizing and weights optimized for Inter */
+ --text-sm: 0.8125rem;
+ --text-base: 0.9375rem;
+ --text-lg: 1rem;
+ --text-xl: 1.125rem;
+ --text-2xl: 1.375rem;
+ --text-3xl: 1.75rem;
+ --text-4xl: 2.125rem;
+ --text-5xl: 2.875rem;
+ --text-6xl: 3.625rem;
+ --text-7xl: 4.375rem;
+ --text-8xl: 5.875rem;
+ --text-9xl: 7.875rem;
+ --font-weight-normal: 450;
}
@keyframes shimmer {
@@ -178,12 +197,22 @@ body,
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
overscroll-behavior: none;
+ font-weight: 450;
}
* {
box-sizing: border-box;
}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ @apply font-heading font-semibold;
+}
+
.form-group .form-text,
.Form .form-text {
@apply text-foreground-lighter;
diff --git a/apps/studio/tests/components/Auth/Auth.constants.test.ts b/apps/studio/tests/components/Auth/Auth.constants.test.ts
index f618b77136d..1beb569ed1d 100644
--- a/apps/studio/tests/components/Auth/Auth.constants.test.ts
+++ b/apps/studio/tests/components/Auth/Auth.constants.test.ts
@@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest'
-import { urlRegex } from '@/components/interfaces/Auth/Auth.constants'
+import {
+ normalizeRedirectUrl,
+ parseRedirectUrls,
+ urlRegex,
+} from '@/components/interfaces/Auth/Auth.constants'
describe('Auth.constants: urlRegex', () => {
it('should match valid URLs', () => {
@@ -49,4 +53,16 @@ describe('Auth.constants: urlRegex', () => {
const simpleDomainUrl = 'smtp-pulse.com'
expect(urlRegex({ excludeSimpleDomains: false }).test(simpleDomainUrl)).toBe(true)
})
+
+ it('normalizes redirect URLs before saving', () => {
+ expect(normalizeRedirectUrl(' https://example.com/path , ')).toBe('https://example.com/path')
+ })
+
+ it('parses stored redirect URLs into trimmed unique values', () => {
+ expect(
+ parseRedirectUrls(
+ 'https://example.com/callback, https://example.com/callback , https://example.com/next'
+ )
+ ).toEqual(['https://example.com/callback', 'https://example.com/next'])
+ })
})
diff --git a/apps/www/app/(home)/ThemeForcer.tsx b/apps/www/app/(home)/ThemeForcer.tsx
new file mode 100644
index 00000000000..51679284972
--- /dev/null
+++ b/apps/www/app/(home)/ThemeForcer.tsx
@@ -0,0 +1,8 @@
+'use client'
+
+import { useForceDeepDark } from 'lib/theme.utils'
+
+export function ThemeForcer() {
+ useForceDeepDark()
+ return null
+}
diff --git a/apps/www/app/(home)/_components/BuiltWithSupabaseSection.tsx b/apps/www/app/(home)/_components/BuiltWithSupabaseSection.tsx
new file mode 100644
index 00000000000..e9eb6e8f9c6
--- /dev/null
+++ b/apps/www/app/(home)/_components/BuiltWithSupabaseSection.tsx
@@ -0,0 +1,200 @@
+import Examples from 'data/Examples'
+import Link from 'next/link'
+import { cn } from 'ui'
+
+import SectionContainer from '@/components/Layouts/SectionContainer'
+
+const EXAMPLE_LOGO: Record = {
+ 'Stripe Subscriptions Starter': { src: '/images/logos/frameworks/stripe.svg' },
+ 'Next.js Starter': { src: '/images/logos/frameworks/nextjs.svg', invert: true },
+ 'AI Chatbot': { src: '/images/logos/frameworks/openai.svg', invert: true },
+ 'LangChain + Next.js Starter': { src: '/images/logos/frameworks/langchain.svg' },
+ 'Flutter User Management': { src: '/images/logos/frameworks/flutter.svg' },
+ 'Expo React Native Starter': { src: '/images/logos/frameworks/expo.svg', invert: true },
+}
+
+function SkeletonBar({ className }: { className?: string }) {
+ return
+}
+
+function StripeSkeleton() {
+ return (
+
+ )
+}
+
+function NextjsSkeleton() {
+ return (
+
+ {/* Sidebar */}
+
+ {/* Main */}
+
+
+ {/* 3x4 data grid */}
+
+ {Array.from({ length: 12 }).map((_, i) => (
+
+ ))}
+
+
+
+ )
+}
+
+const SKELETON_CONTENT: Record React.JSX.Element> = {
+ 'Stripe Subscriptions Starter': StripeSkeleton,
+ 'Next.js Starter': NextjsSkeleton,
+}
+
+export function BuiltWithSupabaseSection() {
+ const featuredExamples = Examples.slice(0, 2)
+ const gridExamples = Examples.slice(2, 6)
+
+ return (
+
+ {/* Header row */}
+
+
+ Kickstart your next project
+ with production ready templates
+
+
+ View all examples
+
+
+
+
+ {/* Featured row - 2 large cells */}
+
+ {featuredExamples.map((example) => {
+ const logo = EXAMPLE_LOGO[example.title]
+ const Skeleton = SKELETON_CONTENT[example.title]
+ return (
+
+
+ {logo && (
+
+ )}
+
+
{example.title}
+
+ {example.description}
+
+
+
+
+ {/* Browser frame */}
+
+ {/* Title bar */}
+
+
+
+
+
+ {/* Skeleton content */}
+
+ {Skeleton ? : null}
+
+
+
+
+ )
+ })}
+
+
+ {/* Grid row - 4 smaller cells */}
+
+ {gridExamples.map((example) => {
+ const logo = EXAMPLE_LOGO[example.title]
+ return (
+
+
+ {logo && (
+
+ )}
+
+
{example.title}
+
+ {example.description}
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/CTASection.tsx b/apps/www/app/(home)/_components/CTASection.tsx
new file mode 100644
index 00000000000..346b93b89f2
--- /dev/null
+++ b/apps/www/app/(home)/_components/CTASection.tsx
@@ -0,0 +1,48 @@
+'use client'
+
+import { useSendTelemetryEvent } from '~/lib/telemetry'
+import Link from 'next/link'
+import { Button } from 'ui'
+
+export function CTASection() {
+ const sendTelemetryEvent = useSendTelemetryEvent()
+
+ return (
+
+
+
+ Build in a weekend,
+ scale to millions
+
+
+
+
+ sendTelemetryEvent({
+ action: 'start_project_button_clicked',
+ properties: { buttonLocation: 'CTA Banner' },
+ })
+ }
+ >
+ Start your project
+
+
+
+
+ sendTelemetryEvent({
+ action: 'request_demo_button_clicked',
+ properties: { buttonLocation: 'CTA Banner' },
+ })
+ }
+ >
+ Request a demo
+
+
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/CommunitySectionV2.tsx b/apps/www/app/(home)/_components/CommunitySectionV2.tsx
new file mode 100644
index 00000000000..1025a413d27
--- /dev/null
+++ b/apps/www/app/(home)/_components/CommunitySectionV2.tsx
@@ -0,0 +1,113 @@
+'use client'
+
+import { AnimatePresence, motion } from 'framer-motion'
+import Link from 'next/link'
+import { useEffect, useState } from 'react'
+import { topTweets } from 'shared-data/tweets'
+import { Button } from 'ui'
+import { TweetCard } from 'ui-patterns/TweetCard'
+
+const COLS = 5
+const TWEETS_PER_PAGE = 15
+
+// V-shape offsets: outer columns high, center lowest
+const COL_OFFSETS = [0, 60, 100, 60, 0]
+
+function distributeToColumns(tweets: typeof topTweets, cols: number) {
+ const columns: (typeof topTweets)[] = Array.from({ length: cols }, () => [])
+ tweets.forEach((tweet, i) => columns[i % cols].push(tweet))
+ return columns
+}
+
+export function CommunitySectionV2() {
+ const [page, setPage] = useState(0)
+
+ useEffect(() => {
+ if (topTweets.length === 0) return
+ const interval = setInterval(() => {
+ setPage((p) => p + 1)
+ }, 10000)
+ return () => clearInterval(interval)
+ }, [])
+
+ if (topTweets.length === 0) return null
+
+ const currentTweets = Array.from({ length: TWEETS_PER_PAGE }, (_, i) => {
+ const idx = (page * TWEETS_PER_PAGE + i) % topTweets.length
+ return topTweets[idx]
+ })
+
+ const columns = distributeToColumns(currentTweets, COLS)
+
+ return (
+
+
+
+
Join the community
+
+ Discover what our community has to say about their Supabase experience.
+
+
+
+ Join us on Discord
+
+
+
+
+ {/* Decorative social proof — hidden from a11y tree, links not keyboard-reachable to prevent focus trap */}
+
+
+
+ {columns.map((col, colIdx) => (
+
+ {col.map((tweet, i) => (
+
+
+
+
+
+ ))}
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/CustomerStoriesSection.tsx b/apps/www/app/(home)/_components/CustomerStoriesSection.tsx
new file mode 100644
index 00000000000..380ae364eb3
--- /dev/null
+++ b/apps/www/app/(home)/_components/CustomerStoriesSection.tsx
@@ -0,0 +1,340 @@
+'use client'
+
+import { motion } from 'framer-motion'
+import Link from 'next/link'
+import { useState } from 'react'
+import { cn } from 'ui'
+
+import SectionContainer from '@/components/Layouts/SectionContainer'
+
+const customerStories = [
+ {
+ name: 'Firecrawl',
+ logo: '/images/customers/logos/firecrawl.png',
+ icon: '/images/customers/logos/firecrawl-icon.svg',
+ tagline: 'Switched from Pinecone to Supabase Vector to boost efficiency and accuracy.',
+ quote:
+ "We looked at the alternatives and chose Supabase because it's open source, it's simpler, and for all the ways we need to use it, Supabase has been just as performant — if not more performant — than the other vector databases.",
+ author: 'Caleb Peffer, CEO, Firecrawl',
+ authorImg: '/images/blog/avatars/caleb-peffer.jpg',
+ slug: 'firecrawl',
+ rawIcon: true,
+ iconFilter: 'brightness(0) invert(1)',
+ bgColor: 'color(display-p3 0.980392 0.364706 0.098039)',
+ bgGradient:
+ 'linear-gradient(to bottom left, color(display-p3 0.980392 0.364706 0.098039 / 1) 0%, color(display-p3 0.980392 0.364706 0.098039 / 0.9) 100%)',
+ dimBgColor: 'color(display-p3 0.980392 0.364706 0.098039 / 0.9)',
+ textColor: 'light' as 'light' | 'dark',
+ },
+ {
+ name: 'Rally',
+ logo: '/images/customers/logos/rally.png',
+ icon: '/images/customers/logos/rally-icon.svg',
+ rawIcon: true,
+ tagline: 'From first line of code to fully licensed fintech in three months.',
+ quote:
+ "We could not have built this company without Supabase. If I had to go and build all these components myself, we wouldn't even have launched.",
+ author: 'Thiago Peres, Founder & CTO, Rally',
+ authorImg: '/images/blog/avatars/thiago-peres-rally.jpeg',
+ slug: 'rally',
+ bgColor: 'color(display-p3 0.275 0.306 0.8)',
+ bgGradient:
+ 'linear-gradient(to bottom left, color(display-p3 0.275 0.306 0.8 / 1) 0%, color(display-p3 0.118 0.176 0.769 / 1) 100%)',
+ dimBgColor: 'color(display-p3 0.118 0.176 0.769 / 1)',
+ textColor: 'light' as 'light' | 'dark',
+ },
+ {
+ name: 'Hyper',
+ logo: '/images/customers/logos/hyper.svg',
+ icon: '/images/customers/logos/hyper-icon.svg',
+ tagline: 'An AI-native marketing platform with agents that operate across the entire workflow.',
+ quote:
+ 'I will get on a podcast and talk about how much I love Supabase. With Supabase we can move fast and build things that delight our customers without having to worry about infrastructure.',
+ author: 'Elliot Fleck, Co-founder, Hyper',
+ authorImg: '/images/blog/avatars/elliot-fleck-hyper.jpeg',
+ slug: 'hyper',
+ bgColor: '#222222',
+ bgGradient: 'linear-gradient(to bottom left, #2a2a2a 0%, #181818 100%)',
+ dimBgColor: '#1e1e1e',
+ textColor: 'light' as 'light' | 'dark',
+ },
+ {
+ name: 'E2B',
+ logo: '/images/customers/logos/e2b.png',
+ icon: '/images/customers/logos/e2b-icon.svg',
+ tagline: 'Secure, scalable execution of AI-generated code in the cloud.',
+ quote:
+ "Supabase empowers us to focus on innovation rather than infrastructure. It's the backbone of our platform, enabling scalability and seamless developer experiences.",
+ author: 'Vasek Mlejnsky, CEO, E2B',
+ authorImg: '/images/blog/avatars/vasek-mlejnsky.jpg',
+ slug: 'e2b',
+ rawIcon: true,
+ iconFilter: 'brightness(0) invert(1)',
+ bgColor: 'color(display-p3 1 0.533 0)',
+ bgGradient:
+ 'linear-gradient(to bottom left, color(display-p3 1 0.533 0 / 1) 0%, color(display-p3 0.7 0.373 0 / 1) 100%)',
+ dimBgColor: 'color(display-p3 0.7 0.373 0 / 1)',
+ textColor: 'light' as 'light' | 'dark',
+ },
+ {
+ name: 'Mobbin',
+ logo: '/images/customers/logos/mobbin.png',
+ icon: '/images/customers/logos/mobbin-icon.svg',
+ tagline: 'Migrated 200,000 users from Firebase for a better authentication experience.',
+ quote:
+ 'Migrating to Supabase meant that we could instantly fix our Auth problems and save money. Just being on Supabase alone gives us confidence we can deliver on whatever users need in the future.',
+ author: 'Jian Jie Liau, Co-founder & CTO, Mobbin',
+ authorImg: '/images/blog/avatars/jian-mobbin.jpg',
+ slug: 'mobbin',
+ bgColor: '#000000',
+ bgGradient: 'linear-gradient(to bottom left, #0a0a0a 0%, #000000 100%)',
+ dimBgColor: '#080808',
+ textColor: 'light' as 'light' | 'dark',
+ },
+]
+
+function IconChip({
+ story,
+ size = 'md',
+}: {
+ story: (typeof customerStories)[0]
+ size?: 'sm' | 'md'
+}) {
+ const isLight = story.textColor === 'light'
+ const s = story as any
+ const filter =
+ s.iconFilter ?? (s.rawIcon ? undefined : isLight ? 'brightness(0) invert(1)' : 'brightness(0)')
+ return (
+
+ )
+}
+
+// Change INACTIVE_PAD to adjust closed column width automatically
+const INACTIVE_PAD = 22 // px padding on each side of icon in closed columns
+const ACTIVE_PAD = 32 // px padding on each side when card is open
+const ICON_PX = 32 // w-8 = 32px
+const INACTIVE_COL_WIDTH = INACTIVE_PAD * 2 + ICON_PX // = 76px
+
+export function CustomerStoriesSection() {
+ const [activeIdx, setActiveIdx] = useState(0)
+ const active = customerStories[activeIdx]
+
+ return (
+
+ {/* Header row */}
+
+
+
+ How industry leaders {' '}
+ are building with Supabase
+
+
+ More customer stories
+
+
+
+
+ {/* Cards row */}
+
+ {/* Mobile: stacked cards */}
+
+ {customerStories.map((story, index) => {
+ const isActive = index === activeIdx
+ const isDark = story.textColor === 'dark'
+ return (
+
setActiveIdx(index)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ setActiveIdx(index)
+ }
+ }}
+ className="text-left rounded-lg p-5 flex flex-col gap-4 overflow-hidden transition-opacity cursor-pointer"
+ style={{ background: isActive ? story.bgGradient : story.dimBgColor }}
+ >
+
+ {isActive && (
+
+
+
+ {story.name}
+
+
+ {story.tagline}
+
+
+
+ {story.quote}
+
+
+
+
+ {story.author}
+
+
+
+ Read the story →
+
+
+ )}
+
+ )
+ })}
+
+
+ {/* Desktop: animated accordion grid */}
+ (i === activeIdx ? '1fr' : `${INACTIVE_COL_WIDTH}px`))
+ .join(' '),
+ }}
+ >
+ {customerStories.map((story, index) => {
+ const isActive = index === activeIdx
+ const isDark = story.textColor === 'dark'
+ return (
+
setActiveIdx(index)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ setActiveIdx(index)
+ }
+ }}
+ className="text-left flex flex-col items-start gap-8 overflow-hidden cursor-pointer"
+ style={{
+ background: story.bgGradient,
+ borderRadius: 8,
+ padding: isActive ? ACTIVE_PAD : `${ACTIVE_PAD}px ${INACTIVE_PAD}px`,
+ boxShadow: isDark
+ ? 'inset 0 0 0 1px rgba(0,0,0,0.07)'
+ : 'inset 0 0 0 1px rgba(255,255,255,0.12)',
+ }}
+ transition={{ type: 'spring', duration: 0.4, bounce: 0 }}
+ >
+
+
+
+
+
+ {/* Top: company name + tagline */}
+
+
+ {story.name}
+
+
+ {story.tagline}
+
+
+
+ {/* Bottom: quote + author */}
+
+
+ {story.quote}
+
+
+
+
+ {story.author}
+
+
+
+ Read the story →
+
+
+
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/DashboardFeaturesSection.tsx b/apps/www/app/(home)/_components/DashboardFeaturesSection.tsx
new file mode 100644
index 00000000000..0bafcc2e08e
--- /dev/null
+++ b/apps/www/app/(home)/_components/DashboardFeaturesSection.tsx
@@ -0,0 +1,144 @@
+'use client'
+
+import BrowserFrame from '~/components/BrowserFrame'
+import { AnimatePresence, motion, useInView } from 'framer-motion'
+import { useTheme } from 'next-themes'
+import Link from 'next/link'
+import React, { useRef, useState } from 'react'
+import { cn } from 'ui'
+
+import SectionContainer from '@/components/Layouts/SectionContainer'
+
+const dashboardStories = [
+ {
+ logo: '/images/customers/logos/hyper-icon.svg',
+ name: 'Hyper',
+ quote:
+ 'We store embeddings in a PostgreSQL database, hosted by Supabase, to perform a similarity search to identify the most relevant sections within the MDN.',
+ author: 'Hermina Condei, Director at MDN, Mozilla',
+ slug: 'mozilla-mdn',
+ },
+ {
+ logo: '/images/customers/logos/chatbase-icon.svg',
+ name: 'Chatbase',
+ quote:
+ 'The SQL Editor in Supabase is my favorite feature — it lets us run complex queries, debug issues, and iterate on our schema without ever leaving the browser.',
+ author: 'Yasser Elsaid, Founder of Chatbase',
+ slug: 'chatbase',
+ },
+ {
+ logo: '/images/customers/logos/markprompt-icon.svg',
+ name: 'Markprompt',
+ quote:
+ 'Row Level Security in Supabase allows us to set granular access policies directly in Postgres, so our multi-tenant architecture stays secure by default.',
+ author: 'Michael Fester, Co-Founder of Markprompt',
+ slug: 'markprompt',
+ },
+]
+
+export function DashboardFeaturesSection({
+ title,
+ tabs,
+}: {
+ title: React.ReactNode
+ tabs: {
+ label: string
+ panel: React.FC<{ isDark: boolean }>
+ highlights: { label: string; link?: string }[]
+ }[]
+}) {
+ const { resolvedTheme } = useTheme()
+ const [activeTabIdx, setActiveTabIdx] = useState(0)
+ const sectionRef = useRef(null)
+ const isInView = useInView(sectionRef, { once: true })
+
+ if (tabs.length === 0) return null
+
+ const Panel = tabs[activeTabIdx].panel
+ const story = dashboardStories[activeTabIdx] ?? dashboardStories[0]
+
+ return (
+ <>
+
+
+
{title}
+
+ {tabs.map((tab, index) => (
+ setActiveTabIdx(index)}
+ aria-selected={index === activeTabIdx}
+ role="tab"
+ className={cn(
+ 'py-1.5 px-3 lg:py-2 lg:px-8 border rounded-full bg-alternative hover:border-foreground text-sm opacity-80 transition-all',
+ 'hover:border-foreground-lighter hover:text-foreground',
+ index === activeTabIdx ? 'opacity-100 !border-foreground' : ''
+ )}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+
+ {isInView && (
+
+
+
+
+
+ )}
+
+
+
+ {/* Customer story banner */}
+
+
+
+
+
+
+
+ {story.name}
+ {' — '}
+ {story.quote} {story.author}
+
+
+
+
+ View more about {story.name}
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/www/app/(home)/_components/FrameworksSection.tsx b/apps/www/app/(home)/_components/FrameworksSection.tsx
new file mode 100644
index 00000000000..a4dc4668478
--- /dev/null
+++ b/apps/www/app/(home)/_components/FrameworksSection.tsx
@@ -0,0 +1,350 @@
+import { createHighlighter, type ThemeRegistration } from 'shiki'
+
+import { FrameworksSectionClient } from './FrameworksSectionClient'
+
+// ── Shiki themes ────────────────────────────────────────────────────────────
+
+const supabaseDark: ThemeRegistration = {
+ name: 'supabase-dark',
+ type: 'dark',
+ colors: {
+ 'editor.background': '#00000000',
+ 'editor.foreground': '#ffffff',
+ },
+ tokenColors: [
+ {
+ scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
+ settings: { foreground: '#bda4ff' },
+ },
+ {
+ scope: [
+ 'entity.name.function',
+ 'support.function',
+ 'entity.name.tag',
+ 'support.class.component',
+ ],
+ settings: { foreground: '#3ecf8e' },
+ },
+ {
+ scope: ['constant', 'variable.other.constant', 'support.constant'],
+ settings: { foreground: '#3ecf8e' },
+ },
+ {
+ scope: [
+ 'variable.other.property',
+ 'support.type.property-name',
+ 'meta.object-literal.key',
+ 'entity.other.attribute-name',
+ ],
+ settings: { foreground: '#3ecf8e' },
+ },
+ { scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
+ { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
+ { scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
+ { scope: ['punctuation'], settings: { foreground: '#ffffff' } },
+ { scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
+ { scope: ['markup.underline.link'], settings: { foreground: '#ffffff' } },
+ { scope: ['markup.inserted'], settings: { foreground: '#3ecf8e' } },
+ { scope: ['markup.deleted'], settings: { foreground: '#F06A50' } },
+ ],
+}
+
+const supabaseLight: ThemeRegistration = {
+ name: 'supabase-light',
+ type: 'light',
+ colors: {
+ 'editor.background': '#00000000',
+ 'editor.foreground': '#525252',
+ },
+ tokenColors: [
+ {
+ scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'],
+ settings: { foreground: '#6b35dc' },
+ },
+ {
+ scope: [
+ 'entity.name.function',
+ 'support.function',
+ 'entity.name.tag',
+ 'support.class.component',
+ ],
+ settings: { foreground: '#15593b' },
+ },
+ {
+ scope: ['constant', 'variable.other.constant', 'support.constant'],
+ settings: { foreground: '#15593b' },
+ },
+ {
+ scope: [
+ 'variable.other.property',
+ 'support.type.property-name',
+ 'meta.object-literal.key',
+ 'entity.other.attribute-name',
+ ],
+ settings: { foreground: '#15593b' },
+ },
+ { scope: ['string', 'string.quoted'], settings: { foreground: '#b45309' } },
+ { scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
+ { scope: ['variable.parameter'], settings: { foreground: '#525252' } },
+ { scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
+ { scope: ['constant.numeric'], settings: { foreground: '#525252' } },
+ { scope: ['markup.underline.link'], settings: { foreground: '#525252' } },
+ ],
+}
+
+// ── Framework data ──────────────────────────────────────────────────────────
+
+const frameworksList: {
+ name: string
+ icon: string
+ code: string
+ lang: 'javascript' | 'dart' | 'svelte' | 'vue'
+ docsUrl: string
+}[] = [
+ {
+ name: 'React',
+ icon: 'M45.74 23.6983C45.2739 23.5379 44.7909 23.3861 44.2937 23.2426C44.3754 22.909 44.4504 22.5798 44.5171 22.2561C45.6119 16.9418 44.8961 12.6605 42.4518 11.2509C40.1079 9.89927 36.2748 11.3085 32.4035 14.6776C32.0313 15.0016 31.6579 15.3446 31.2848 15.704C31.0362 15.4662 30.7879 15.2364 30.5403 15.0165C26.4831 11.4141 22.4164 9.89599 19.9744 11.3097C17.6329 12.6652 16.9394 16.69 17.9249 21.7265C18.0201 22.2129 18.1313 22.7097 18.2571 23.2148C17.6816 23.3782 17.1259 23.5524 16.5943 23.7377C11.8376 25.3961 8.7998 27.9952 8.7998 30.6911C8.7998 33.4755 12.0609 36.2683 17.0153 37.9617C17.4063 38.0953 17.812 38.2217 18.2301 38.3416C18.0944 38.8879 17.9763 39.4232 17.8773 39.9454C16.9376 44.8944 17.6714 48.8242 20.0068 50.1711C22.4189 51.5622 26.4673 50.1324 30.4093 46.6865C30.7209 46.4141 31.0336 46.1253 31.3469 45.8225C31.7529 46.2135 32.1582 46.5835 32.5615 46.9306C36.3798 50.2164 40.151 51.5432 42.4842 50.1925C44.894 48.7975 45.6772 44.576 44.6604 39.4399C44.5828 39.0476 44.4924 38.6469 44.3909 38.239C44.6752 38.155 44.9543 38.0682 45.2265 37.978C50.3771 36.2715 53.7282 33.5127 53.7282 30.6911C53.7282 27.9854 50.5924 25.3688 45.74 23.6983ZM44.6228 36.1561C44.3772 36.2375 44.1251 36.3161 43.8682 36.3923C43.2996 34.5922 42.5322 32.6781 41.5931 30.7005C42.4893 28.7699 43.227 26.8803 43.7797 25.0919C44.2393 25.2249 44.6854 25.3651 45.1152 25.5132C49.2728 26.9444 51.8089 29.0605 51.8089 30.6911C51.8089 32.4279 49.07 34.6826 44.6228 36.1561ZM42.7776 39.8126C43.2272 42.0837 43.2914 44.1371 42.9936 45.7423C42.726 47.1847 42.1878 48.1463 41.5225 48.5315C40.1066 49.351 37.0787 48.2857 33.8132 45.4757C33.4388 45.1535 33.0618 44.8096 32.6835 44.4455C33.9495 43.061 35.2147 41.4514 36.4495 39.6638C38.6215 39.4711 40.6735 39.156 42.5344 38.7258C42.626 39.0955 42.7074 39.4581 42.7776 39.8126ZM24.1169 48.3898C22.7336 48.8784 21.6318 48.8924 20.9658 48.5084C19.5486 47.691 18.9594 44.5358 19.7631 40.3033C19.8551 39.8186 19.9647 39.3207 20.091 38.8118C21.9314 39.2187 23.9684 39.5116 26.1456 39.6881C27.3887 41.4373 28.6905 43.0452 30.0024 44.453C29.7157 44.7297 29.4302 44.9931 29.1463 45.2413C27.4032 46.7651 25.6564 47.8461 24.1169 48.3898ZM17.6361 36.1455C15.4453 35.3967 13.6361 34.4235 12.396 33.3616C11.2817 32.4073 10.7191 31.4599 10.7191 30.6911C10.7191 29.0551 13.1581 26.9684 17.226 25.5501C17.7196 25.378 18.2363 25.2158 18.7725 25.0635C19.3347 26.8923 20.0722 28.8043 20.9623 30.7378C20.0607 32.7 19.3128 34.6425 18.745 36.4927C18.3628 36.3829 17.9924 36.2672 17.6361 36.1455ZM19.8085 21.3579C18.9642 17.0428 19.5249 13.7876 20.936 12.9708C22.4391 12.1006 25.7628 13.3413 29.2659 16.4518C29.4898 16.6506 29.7146 16.8587 29.9401 17.074C28.6347 18.4756 27.3448 20.0714 26.1127 21.8103C23.9997 22.0061 21.977 22.3208 20.1174 22.742C20.0004 22.2717 19.8969 21.8097 19.8085 21.3579ZM39.1886 26.1433C38.744 25.3754 38.2876 24.6257 37.8223 23.8964C39.2558 24.0777 40.6293 24.3182 41.9191 24.6126C41.5318 25.8536 41.0492 27.1511 40.4811 28.4813C40.0735 27.7076 39.6425 26.9275 39.1886 26.1433ZM31.2854 18.4456C32.1707 19.4047 33.0573 20.4756 33.9293 21.6374C33.0506 21.5959 32.161 21.5743 31.264 21.5743C30.3755 21.5743 29.4925 21.5954 28.6192 21.6362C29.4921 20.4852 30.3863 19.4158 31.2854 18.4456ZM23.3317 26.1566C22.8876 26.9267 22.4645 27.7025 22.0634 28.4799C21.5045 27.1543 21.0263 25.8509 20.6357 24.5923C21.9176 24.3054 23.2846 24.0709 24.7089 23.8931C24.2371 24.6291 23.7769 25.3843 23.3317 26.1564V26.1566ZM24.75 37.626C23.2783 37.4618 21.8908 37.2394 20.6093 36.9604C21.0061 35.6793 21.4948 34.3481 22.0655 32.994C22.4677 33.7707 22.8925 34.5469 23.3393 35.3187H23.3393C23.7945 36.1049 24.266 36.875 24.75 37.626ZM31.3385 43.0719C30.4289 42.0904 29.5215 41.0047 28.6353 39.8368C29.4956 39.8706 30.3726 39.8879 31.264 39.8879C32.1798 39.8879 33.085 39.8672 33.9761 39.8276C33.1012 41.0164 32.2178 42.1038 31.3385 43.0719ZM40.4994 32.9249C41.0999 34.2937 41.6061 35.618 42.0081 36.8772C40.7054 37.1744 39.2989 37.4138 37.8171 37.5916C38.2835 36.8525 38.7439 36.0899 39.1963 35.3055C39.6539 34.5118 40.0885 33.717 40.4994 32.9249ZM37.5337 34.3466C36.8314 35.5643 36.1104 36.7268 35.3784 37.8241C34.0452 37.9194 32.6678 37.9685 31.264 37.9685C29.8659 37.9685 28.5058 37.9251 27.1962 37.8401C26.4347 36.7284 25.698 35.5625 25.0002 34.3571H25.0004C24.3044 33.155 23.6638 31.9427 23.0834 30.7372C23.6636 29.5289 24.3025 28.3152 24.9945 27.1152L24.9944 27.1155C25.6882 25.9123 26.4184 24.7521 27.1729 23.6473C28.509 23.5463 29.8792 23.4936 31.2639 23.4936H31.264C32.655 23.4936 34.0269 23.5467 35.3626 23.6486C36.1056 24.7453 36.8308 25.9017 37.5274 27.1051C38.2319 28.3219 38.879 29.5275 39.4642 30.7099C38.8808 31.9126 38.2351 33.1303 37.5337 34.3466ZM41.4931 12.9137C42.9976 13.7813 43.5826 17.2804 42.6374 21.8688C42.5771 22.1615 42.5092 22.4597 42.4354 22.762C40.5715 22.3319 38.5474 22.0118 36.4282 21.813C35.1937 20.055 33.9143 18.4567 32.6302 17.0731C32.9755 16.741 33.3202 16.4243 33.6636 16.1254C36.9805 13.2388 40.0806 12.0991 41.4931 12.9137ZM31.264 26.6791C33.4797 26.6791 35.276 28.4753 35.276 30.6911C35.276 32.9068 33.4797 34.703 31.264 34.703C29.0483 34.703 27.252 32.9068 27.252 30.6911C27.252 28.4753 29.0483 26.6791 31.264 26.6791Z',
+ code: `import { createClient } from '@supabase/supabase-js'
+
+const supabase = createClient(
+ process.env.SUPABASE_URL,
+ process.env.SUPABASE_ANON_KEY
+)
+
+export default function App() {
+ const [todos, setTodos] = useState([])
+
+ useEffect(() => {
+ supabase.from('todos').select('*')
+ .then(({ data }) => setTodos(data))
+ }, [])
+
+ return
+}`,
+ lang: 'javascript' as const,
+ docsUrl: '/docs/guides/getting-started/quickstarts/reactjs',
+ },
+ {
+ name: 'Next.js',
+ icon: 'M42.3148 48.6796C38.9009 50.9525 34.8014 52.2771 30.3924 52.2771C18.4957 52.2771 8.85156 42.6329 8.85156 30.7362C8.85156 18.8395 18.4957 9.19531 30.3924 9.19531C42.2891 9.19531 51.9333 18.8395 51.9333 30.7362C51.9333 37.1564 49.1245 42.9207 44.6688 46.8671L39.5552 40.2803V21.8278H36.584V36.4531L25.2299 21.8278H21.4808V39.6473H24.4801V25.6368L42.3148 48.6796Z',
+ code: `import { createClient } from '@supabase/supabase-js'
+
+const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
+)
+
+export default async function Page() {
+ const { data: todos } = await supabase
+ .from('todos')
+ .select('*')
+
+ return
+}`,
+ lang: 'javascript' as const,
+ docsUrl: '/docs/guides/getting-started/quickstarts/nextjs',
+ },
+ {
+ name: 'Flutter',
+ icon: 'M46.5067 10.3828L34.3509 10.3962L14.75 29.9971L20.7974 36.0519L26.1125 30.7666L46.5067 10.3828Z M34.6996 28.4653C34.5272 28.4573 34.3493 28.4491 34.2378 28.5965L23.7856 39.0471L29.7894 45.0142L29.7825 45.021L34.079 49.3212C34.1072 49.3462 34.1352 49.3741 34.1637 49.4026C34.2813 49.5201 34.4074 49.6462 34.5895 49.6055C36.5743 49.601 38.5591 49.6017 40.544 49.6025C42.529 49.6032 44.5142 49.604 46.4998 49.5995L35.9333 39.0234L46.4963 28.467L34.906 28.464C34.8415 28.4719 34.7711 28.4686 34.6996 28.4653Z',
+ code: `import 'package:supabase_flutter/supabase_flutter.dart';
+
+Future main() async {
+ await Supabase.initialize(
+ url: 'YOUR_SUPABASE_URL',
+ anonKey: 'YOUR_SUPABASE_ANON_KEY',
+ );
+ runApp(MyApp());
+}
+
+final supabase = Supabase.instance.client;
+
+final data = await supabase
+ .from('todos')
+ .select('*');`,
+ lang: 'dart' as const,
+ docsUrl: '/docs/guides/getting-started/quickstarts/flutter',
+ },
+ {
+ name: 'Svelte',
+ icon: 'M29.9094 11.2292C35.956 7.37668 44.3187 9.17299 48.553 15.2334H48.5532C50.5831 18.0746 51.3826 21.614 50.771 25.0519C50.4778 26.677 49.8581 28.2259 48.9493 29.6047C50.2752 32.1335 50.7201 35.0322 50.2136 37.8422C49.6086 41.2154 47.6106 44.1777 44.7096 46.0024L34.0903 52.7707C28.0445 56.623 19.6818 54.8274 15.4466 48.7665C13.4171 45.9251 12.6176 42.3859 13.2288 38.948C13.5223 37.3227 14.1422 35.7738 15.0512 34.3949C13.7247 31.8665 13.2794 28.9677 13.786 26.1577C14.3913 22.7845 16.3893 19.8223 19.29 17.9974L29.9094 11.2292ZM19.8146 45.9861C21.8311 48.8931 25.4469 50.2333 28.8709 49.343H28.8708C29.6345 49.139 30.3624 48.8192 31.0293 48.3946L41.6512 41.6252C43.396 40.5274 44.5979 38.7455 44.9622 36.7164C45.33 34.6483 44.8489 32.5192 43.6278 30.8101C41.6113 27.9032 37.9955 26.5629 34.5715 27.4531C33.8084 27.6571 33.081 27.9768 32.4147 28.4012L28.3617 30.9842C28.1601 31.1125 27.9401 31.2092 27.7093 31.271C26.6776 31.5384 25.5887 31.1342 24.9815 30.2584C24.614 29.7429 24.4693 29.1012 24.5801 28.4779C24.6899 27.8669 25.0519 27.3302 25.5774 26.9996L36.2002 20.2298C36.4017 20.1015 36.6218 20.0048 36.8526 19.9431C37.8838 19.6754 38.9725 20.0795 39.5793 20.9551C39.9039 21.4146 40.0556 21.974 40.0078 22.5345L39.9714 22.9285L40.3662 23.0484C41.8596 23.4989 43.265 24.2014 44.5218 25.1254L45.0657 25.5245L45.2658 24.9145C45.3729 24.59 45.4577 24.2586 45.5196 23.9225C45.8873 21.8544 45.4063 19.7254 44.1852 18.0162C42.1687 15.1093 38.553 13.7691 35.129 14.6593C34.3653 14.8633 33.6374 15.1832 32.9705 15.6077L22.3487 22.3777C20.6036 23.475 19.4016 25.2568 19.0376 27.2858C18.6699 29.3539 19.1509 31.4829 20.372 33.192C22.3885 36.099 26.0043 37.4392 29.4283 36.549C30.1914 36.345 30.9188 36.0256 31.5853 35.6017L35.6389 33.0177C35.8402 32.8895 36.06 32.7929 36.2905 32.7311C37.3221 32.4637 38.4111 32.868 39.0183 33.7438C39.3857 34.2592 39.5306 34.9007 39.4205 35.524C39.3102 36.1352 38.948 36.6718 38.4224 37.0028L27.7996 43.7722C27.5981 43.9006 27.378 43.9973 27.1471 44.059C26.116 44.3266 25.0273 43.9225 24.4204 43.0469C24.0955 42.5876 23.9438 42.0281 23.992 41.4675L24.0284 41.0735L23.6336 40.9537C22.1404 40.5032 20.7351 39.8011 19.4783 38.8776L18.934 38.4778L18.734 39.0878C18.6266 39.4122 18.5418 39.7437 18.4801 40.0798C18.1125 42.1479 18.5935 44.277 19.8146 45.9861Z',
+ code: ``,
+ lang: 'svelte' as const,
+ docsUrl: '/docs/guides/getting-started/quickstarts/sveltekit',
+ },
+ {
+ name: 'Vue',
+ icon: 'M43.0532 13.4531H50.1147L30.2756 47.8158L10.4365 13.4531H17.4978L30.2755 35.5845L43.0532 13.4531ZM42.1764 13.4531L30.2755 34.0659L18.3746 13.4531L25.6939 13.4531L30.2756 21.3888L34.8572 13.4531L42.1764 13.4531Z',
+ code: ``,
+ lang: 'vue' as const,
+ docsUrl: '/docs/guides/getting-started/quickstarts/vuejs',
+ },
+ {
+ name: 'Nuxt',
+ icon: 'M32.5784 45.4741H50.2199C50.7802 45.4741 51.3307 45.3325 51.8159 45.0634C52.3012 44.7943 52.7041 44.4072 52.9842 43.9409C53.2642 43.4748 53.4115 42.946 53.4113 42.4078C53.4111 41.8696 53.2633 41.3409 52.9828 40.875L41.1352 21.164C40.8552 20.6979 40.4524 20.3109 39.9672 20.0418C39.4821 19.7727 38.9317 19.631 38.3715 19.631C37.8113 19.631 37.261 19.7727 36.7758 20.0418C36.2906 20.3109 35.8878 20.6979 35.6078 21.164L32.5784 26.2073L26.6555 16.3452C26.3753 15.8792 25.9723 15.4922 25.487 15.2232C25.0017 14.9541 24.4513 14.8125 23.8909 14.8125C23.3306 14.8125 22.7802 14.9541 22.2949 15.2232C21.8096 14.9541 21.4066 15.8792 21.1263 16.3452L6.38358 40.875C6.10311 41.3409 5.95532 41.8696 5.95508 42.4078C5.95483 42.946 6.10214 43.4748 6.38219 43.9409C6.66224 44.4072 7.06515 44.7943 7.5504 45.0634C8.03564 45.3325 8.58612 45.4741 9.14645 45.4741H20.2203C24.6079 45.4741 27.8436 43.6229 30.07 40.0113L38.3706 26.2073L47.0599 40.6619H35.4754L32.5784 45.4741Z',
+ code: `
+
+
+
+ `,
+ lang: 'vue' as const,
+ docsUrl: '/docs/guides/getting-started/quickstarts/nuxtjs',
+ },
+]
+
+// ── Example links per framework ──────────────────────────────────────────────
+
+const frameworkExamples: Record<
+ string,
+ { title: string; description: string; url: string; icon: string }[]
+> = {
+ React: [
+ {
+ title: 'Todo List',
+ description: 'CRUD app with real-time and RLS.',
+ url: '/docs/guides/getting-started/tutorials/with-react',
+ icon: 'ListChecks',
+ },
+ {
+ title: 'Auth & User Management',
+ description: 'Email, social login, and user profiles.',
+ url: '/docs/guides/auth/quickstarts/react',
+ icon: 'Users',
+ },
+ ],
+ 'Next.js': [
+ {
+ title: 'App Router Quickstart',
+ description: 'Server Components with SSR auth.',
+ url: '/docs/guides/getting-started/quickstarts/nextjs',
+ icon: 'Rocket',
+ },
+ {
+ title: 'Auth with PKCE',
+ description: 'Secure auth flow with middleware.',
+ url: '/docs/guides/auth/server-side/nextjs',
+ icon: 'KeyRound',
+ },
+ ],
+ Flutter: [
+ {
+ title: 'User Management',
+ description: 'Auth and profile management.',
+ url: '/docs/guides/getting-started/tutorials/with-flutter',
+ icon: 'Users',
+ },
+ {
+ title: 'Deep Linking',
+ description: 'OAuth and magic link for mobile.',
+ url: '/docs/guides/auth/native-mobile-deep-linking',
+ icon: 'Link2',
+ },
+ ],
+ Svelte: [
+ {
+ title: 'Todo List',
+ description: 'Server-side auth and real-time data.',
+ url: '/docs/guides/getting-started/tutorials/with-svelte',
+ icon: 'ListChecks',
+ },
+ {
+ title: 'Auth Helpers',
+ description: 'Sessions and protected routes.',
+ url: '/docs/guides/auth/server-side/sveltekit',
+ icon: 'ShieldCheck',
+ },
+ ],
+ Vue: [
+ {
+ title: 'Todo List',
+ description: 'CRUD with Vue 3 Composition API.',
+ url: '/docs/guides/getting-started/tutorials/with-vue-3',
+ icon: 'ListChecks',
+ },
+ {
+ title: 'Auth with Vue',
+ description: 'Email and OAuth authentication.',
+ url: '/docs/guides/getting-started/quickstarts/vuejs',
+ icon: 'KeyRound',
+ },
+ ],
+ Nuxt: [
+ {
+ title: 'Nuxt Module',
+ description: 'Auto-configured auth and client.',
+ url: '/docs/guides/getting-started/quickstarts/nuxtjs',
+ icon: 'Blocks',
+ },
+ {
+ title: 'Server Routes',
+ description: 'Service role access from server.',
+ url: '/docs/guides/auth/server-side/nuxt',
+ icon: 'Server',
+ },
+ ],
+}
+
+// ── Server component: pre-highlights all code ───────────────────────────────
+
+export async function FrameworksSection() {
+ const hl = await createHighlighter({
+ themes: [supabaseDark, supabaseLight],
+ langs: ['javascript', 'dart', 'svelte', 'vue'],
+ })
+
+ const frameworks = frameworksList.map((fw) => ({
+ name: fw.name,
+ icon: fw.icon,
+ docsUrl: fw.docsUrl,
+ darkHtml: hl.codeToHtml(fw.code, { lang: fw.lang, theme: 'supabase-dark' }),
+ lightHtml: hl.codeToHtml(fw.code, { lang: fw.lang, theme: 'supabase-light' }),
+ examples: frameworkExamples[fw.name] ?? [],
+ }))
+
+ hl.dispose()
+
+ return
+}
diff --git a/apps/www/app/(home)/_components/FrameworksSectionClient.tsx b/apps/www/app/(home)/_components/FrameworksSectionClient.tsx
new file mode 100644
index 00000000000..2c3c3230f35
--- /dev/null
+++ b/apps/www/app/(home)/_components/FrameworksSectionClient.tsx
@@ -0,0 +1,140 @@
+'use client'
+
+import { AnimatePresence, motion } from 'framer-motion'
+import Link from 'next/link'
+import { useState } from 'react'
+import { cn } from 'ui'
+
+type Framework = {
+ name: string
+ icon: string
+ docsUrl: string
+ darkHtml: string
+ lightHtml: string
+ examples: { title: string; description: string; url: string; icon: string }[]
+}
+
+export function FrameworksSectionClient({ frameworks }: { frameworks: Framework[] }) {
+ const [activeIdx, setActiveIdx] = useState(0)
+ const active = frameworks[activeIdx]
+
+ const handleTabChange = (index: number) => {
+ setActiveIdx(index)
+ }
+
+ return (
+
+
+
+ {/* Left: title */}
+
+
+ Use Supabase with{' '}
+
+
+
+ {active.name}
+
+
+
+
+
+
+ {/* Right: icon tabs + code */}
+
+ {/* 6-col icon row */}
+
+ {frameworks.map((framework, index) => (
+
handleTabChange(index)}
+ className={cn(
+ 'flex items-center justify-center py-4 border-r border-border last:border-r-0 transition-colors',
+ index === activeIdx
+ ? 'bg-surface-75 text-foreground'
+ : 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
+ )}
+ >
+
+
+
+
+ ))}
+
+
+ {/* Code area */}
+
+
+
+
+
+
+
+
+ {`Read docs for ${active.name}`}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/Hero.tsx b/apps/www/app/(home)/_components/Hero.tsx
new file mode 100644
index 00000000000..a994a09a8f2
--- /dev/null
+++ b/apps/www/app/(home)/_components/Hero.tsx
@@ -0,0 +1,56 @@
+'use client'
+
+import { useSendTelemetryEvent } from '~/lib/telemetry'
+import Link from 'next/link'
+import { Button } from 'ui'
+
+import SectionContainer from '@/components/Layouts/SectionContainer'
+
+export function Hero() {
+ const sendTelemetryEvent = useSendTelemetryEvent()
+
+ return (
+
+
+
+
+ Build in a weekend
+ Scale to millions
+
+
+ Start your project with a Postgres database. Add Authentication, Data APIs, Edge
+ Functions, Realtime Data, Storage, and Vector embeddings.
+
+
+
+
+
+ sendTelemetryEvent({
+ action: 'start_project_button_clicked',
+ properties: { buttonLocation: 'Homepage Hero' },
+ })
+ }
+ >
+ Start your project
+
+
+
+
+ sendTelemetryEvent({
+ action: 'request_demo_button_clicked',
+ properties: { buttonLocation: 'Homepage Hero' },
+ })
+ }
+ >
+ Request a demo
+
+
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/HomeContent.tsx b/apps/www/app/(home)/_components/HomeContent.tsx
new file mode 100644
index 00000000000..10a9ca9fae7
--- /dev/null
+++ b/apps/www/app/(home)/_components/HomeContent.tsx
@@ -0,0 +1,54 @@
+'use client'
+
+import getContent from '~/data/home/content'
+import dynamic from 'next/dynamic'
+
+import { BuiltWithSupabaseSection } from './BuiltWithSupabaseSection'
+import { CommunitySectionV2 as CommunitySection } from './CommunitySectionV2'
+import { CTASection } from './CTASection'
+import { CustomerStoriesSection } from './CustomerStoriesSection'
+import { DashboardFeaturesSection } from './DashboardFeaturesSection'
+import { Hero } from './Hero'
+import { LogosGrid } from './LogosGrid'
+import { OpenSourceSection } from './OpenSourceSection'
+
+const Products = dynamic(() => import('~/components/Products/index'))
+
+export function HomeContent({ frameworksSlot }: { frameworksSlot: React.ReactNode }) {
+ const content = getContent()
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/www/app/(home)/_components/LogosGrid.tsx b/apps/www/app/(home)/_components/LogosGrid.tsx
new file mode 100644
index 00000000000..ae94535c54c
--- /dev/null
+++ b/apps/www/app/(home)/_components/LogosGrid.tsx
@@ -0,0 +1,119 @@
+'use client'
+
+import { AnimatePresence, motion } from 'framer-motion'
+import { ComponentProps, useEffect, useState } from 'react'
+
+import {
+ BetasharesLogo,
+ BoltLogo,
+ ChatbaseLogo,
+ FigmaLogo,
+ GithubLogo,
+ GoodtapeLogo,
+ GopuffLogo,
+ GumloopLogo,
+ HappyteamsLogo,
+ HumataLogo,
+ LangchainLogo,
+ LoopsLogo,
+ LovableLogo,
+ MarkpromptLogo,
+ MdnLogo,
+ MobbinLogo,
+ MozillaLogo,
+ PikaLogo,
+ PwcLogo,
+ ResendLogo,
+ SoshiLogo,
+ SubmagicLogo,
+ TempoLogo,
+ V0Logo,
+} from './logos/PublicityLogos'
+import SectionContainer from '@/components/Layouts/SectionContainer'
+
+const gridLogos: { name: string; Logo: (props: ComponentProps<'svg'>) => React.JSX.Element }[] = [
+ { name: 'betashares', Logo: BetasharesLogo },
+ { name: 'bolt', Logo: BoltLogo },
+ { name: 'chatbase', Logo: ChatbaseLogo },
+ { name: 'figma', Logo: FigmaLogo },
+ { name: 'github', Logo: GithubLogo },
+ { name: 'goodtape', Logo: GoodtapeLogo },
+ { name: 'gopuff', Logo: GopuffLogo },
+ { name: 'gumloop', Logo: GumloopLogo },
+ { name: 'happyteams', Logo: HappyteamsLogo },
+ { name: 'humata', Logo: HumataLogo },
+ { name: 'langchain', Logo: LangchainLogo },
+ { name: 'loops', Logo: LoopsLogo },
+ { name: 'lovable', Logo: LovableLogo },
+ { name: 'markprompt', Logo: MarkpromptLogo },
+ { name: 'mdn', Logo: MdnLogo },
+ { name: 'mobbin', Logo: MobbinLogo },
+ { name: 'mozilla', Logo: MozillaLogo },
+ { name: 'pika', Logo: PikaLogo },
+ { name: 'pwc', Logo: PwcLogo },
+ { name: 'resend', Logo: ResendLogo },
+ { name: 'soshi', Logo: SoshiLogo },
+ { name: 'submagic', Logo: SubmagicLogo },
+ { name: 'tempo', Logo: TempoLogo },
+ { name: 'v0', Logo: V0Logo },
+]
+
+const LOGOS_PER_PAGE = 12
+
+export function LogosGrid() {
+ const [page, setPage] = useState(0)
+ const totalPages = Math.ceil(gridLogos.length / LOGOS_PER_PAGE)
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setPage((p) => (p + 1) % totalPages)
+ }, 10000)
+ return () => clearInterval(interval)
+ }, [totalPages])
+
+ const start = page * LOGOS_PER_PAGE
+ const currentLogos = gridLogos.slice(start, start + LOGOS_PER_PAGE)
+
+ return (
+
+
+
+ Trusted by fast-growing companies worldwide
+
+
+
+
+
+
+
+ {currentLogos.map(({ name, Logo }, i) => (
+
+
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/OpenSourceSection.tsx b/apps/www/app/(home)/_components/OpenSourceSection.tsx
new file mode 100644
index 00000000000..be815f3c132
--- /dev/null
+++ b/apps/www/app/(home)/_components/OpenSourceSection.tsx
@@ -0,0 +1,211 @@
+'use client'
+
+import staticContent from '.generated/staticContent/_index.json'
+import { useSendTelemetryEvent } from '~/lib/telemetry'
+import { useInView } from 'framer-motion'
+import Link from 'next/link'
+import { useRef } from 'react'
+import { Button } from 'ui'
+
+import { kFormatter } from '../../../lib/helpers'
+import SectionContainer from '@/components/Layouts/SectionContainer'
+
+// ── Pixel font (5×7 per glyph) ─────────────────────────────────────────────
+
+const GLYPHS: Record = {
+ '0': ['01110', '10001', '10011', '10101', '11001', '10001', '01110'],
+ '1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
+ '2': ['01110', '10001', '00001', '00110', '01000', '10000', '11111'],
+ '3': ['01110', '10001', '00001', '00110', '00001', '10001', '01110'],
+ '4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
+ '5': ['11111', '10000', '11110', '00001', '00001', '10001', '01110'],
+ '6': ['01110', '10001', '10000', '11110', '10001', '10001', '01110'],
+ '7': ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
+ '8': ['01110', '10001', '10001', '01110', '10001', '10001', '01110'],
+ '9': ['01110', '10001', '10001', '01111', '00001', '10001', '01110'],
+ '.': ['00000', '00000', '00000', '00000', '00000', '00000', '00100'],
+ K: ['10001', '10010', '10100', '11000', '10100', '10010', '10001'],
+ ' ': ['00000', '00000', '00000', '00000', '00000', '00000', '00000'],
+}
+
+function textToPixelMask(text: string): Set {
+ const mask = new Set()
+ let cursorX = 0
+ for (const ch of text) {
+ const glyph = GLYPHS[ch]
+ if (!glyph) {
+ cursorX += 3
+ continue
+ }
+ for (let row = 0; row < glyph.length; row++) {
+ for (let col = 0; col < glyph[row].length; col++) {
+ if (glyph[row][col] === '1') mask.add(`${cursorX + col},${row}`)
+ }
+ }
+ cursorX += 6 // 5 wide + 1 gap
+ }
+ return mask
+}
+
+// ── Contribution graph ──────────────────────────────────────────────────────
+
+const CELL = 3
+const GAP = 0.5
+const COLS = 72
+const ROWS = 40
+
+const githubStars = staticContent.githubStars
+const TEXT = kFormatter(githubStars)
+const TEXT_MASK = textToPixelMask(TEXT)
+const TEXT_W = TEXT.length * 6 - 1
+const TEXT_H = 7
+const TEXT_OFFSET_X = Math.floor((COLS - TEXT_W) / 2)
+const TEXT_OFFSET_Y = Math.floor((ROWS - TEXT_H) / 2)
+
+const LEVELS_BG = [
+ 'hsl(var(--background-surface-300) / 0.2)',
+ 'hsl(var(--brand-400) / 0.08)',
+ 'hsl(var(--brand-500) / 0.1)',
+ 'hsl(var(--brand-600) / 0.12)',
+ 'hsl(var(--brand-default) / 0.12)',
+]
+
+const LEVELS_TEXT = [
+ 'hsl(var(--brand-600))',
+ 'hsl(var(--brand-default))',
+ 'hsl(var(--brand-default))',
+ 'hsl(var(--brand-600))',
+ 'hsl(var(--brand-500))',
+]
+
+function makeGrid(): { level: number; isText: boolean }[][] {
+ let seed = 0x9e3779b9
+ const rand = () => {
+ seed ^= seed << 13
+ seed ^= seed >> 17
+ seed ^= seed << 5
+ return (seed >>> 0) / 0xffffffff
+ }
+ return Array.from({ length: COLS }, (_, c) =>
+ Array.from({ length: ROWS }, (_, r) => {
+ const v = rand()
+ const level = v < 0.45 ? 0 : v < 0.65 ? 1 : v < 0.8 ? 2 : v < 0.92 ? 3 : 4
+ const localX = c - TEXT_OFFSET_X
+ const localY = r - TEXT_OFFSET_Y
+ const isText = TEXT_MASK.has(`${localX},${localY}`)
+ return { level, isText }
+ })
+ )
+}
+
+const GRID = makeGrid()
+const SVG_W = COLS * (CELL + GAP) - GAP
+const SVG_H = ROWS * (CELL + GAP) - GAP
+
+function ContribGraph() {
+ const ref = useRef(null)
+ const isInView = useInView(ref, { once: true, margin: '-50px' })
+
+ return (
+
+ {GRID.map((col, c) =>
+ col.map(({ level, isText }, r) => (
+
+ ))
+ )}
+
+ )
+}
+
+// ── Component ───────────────────────────────────────────────────────────────
+
+export function OpenSourceSection() {
+ const sendTelemetryEvent = useSendTelemetryEvent()
+
+ return (
+
+ {/* Contrib graph — right half, full-height bleed (desktop) */}
+
+
+
+
+
+
+
+
+
Open source from day one
+
+ Supabase is built in the open because we believe great developer tools should be
+ transparent, inspectable, and owned by the community. Read, contribute, self-host.
+ You're never locked in, and always in control.
+
+
+
+
+
+
+
+ }
+ >
+ sendTelemetryEvent({ action: 'homepage_github_button_clicked' })}
+ >
+ View on GitHub
+
+
+
+
+
+ {/* Contrib graph — stacked below the text on mobile */}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/logos/FrameworkLogos.tsx b/apps/www/app/(home)/_components/logos/FrameworkLogos.tsx
new file mode 100644
index 00000000000..be93adc90b7
--- /dev/null
+++ b/apps/www/app/(home)/_components/logos/FrameworkLogos.tsx
@@ -0,0 +1,277 @@
+import { ComponentProps } from 'react'
+
+export function DartLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function ExpoLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function FlutterLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function FlutterflowLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function KotlinLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function LangchainLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+ )
+}
+
+export function NestjsLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function NextjsLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function NuxtjsLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function OpenaiLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function ReactLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function ReactiveXLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function RedwoodLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function RemixLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function SolidjsLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+ )
+}
+
+export function StripeLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function SvelteLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function TurbopackLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function VercelLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function VueLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/_components/logos/PublicityLogos.tsx b/apps/www/app/(home)/_components/logos/PublicityLogos.tsx
new file mode 100644
index 00000000000..b262cda4193
--- /dev/null
+++ b/apps/www/app/(home)/_components/logos/PublicityLogos.tsx
@@ -0,0 +1,591 @@
+import { ComponentProps } from 'react'
+
+export function OnePasswordLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function BetasharesLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function BoltLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+ )
+}
+
+export function BrthrsLogo(props: ComponentProps<'svg'>) {
+ return (
+
+ brthrs-logotype-color
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function ChatbaseLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function CoComLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function FigmaLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+export function GithubLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function GoodtapeLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export function GopuffLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function GumloopLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function HappyteamsLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+ )
+}
+
+export function HumataLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function KikiLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function LangchainLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function LoopsLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function LovableLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function MarkpromptLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function MdnLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function MobbinLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+ )
+}
+
+export function MozillaLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function NextdoorlendingLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function PebblelyLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+ )
+}
+
+export function PikaLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function PwcLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function QuivrLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+ )
+}
+
+export function ResendLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function ShotgunLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function SoshiLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function SubmagicLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function TempoLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function UdioLogo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function V0Logo(props: ComponentProps<'svg'>) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/www/app/(home)/layout.tsx b/apps/www/app/(home)/layout.tsx
new file mode 100644
index 00000000000..61677b56677
--- /dev/null
+++ b/apps/www/app/(home)/layout.tsx
@@ -0,0 +1,17 @@
+import Footer from '~/components/Footer'
+import Nav from '~/components/Nav'
+
+import { ThemeForcer } from './ThemeForcer'
+
+export default function HomeLayout({ children }: { children: React.ReactNode }) {
+ return (
+ <>
+
+
+
+ {children}
+
+
+ >
+ )
+}
diff --git a/apps/www/app/(home)/page.tsx b/apps/www/app/(home)/page.tsx
new file mode 100644
index 00000000000..6fd479bb6b8
--- /dev/null
+++ b/apps/www/app/(home)/page.tsx
@@ -0,0 +1,6 @@
+import { FrameworksSection } from './_components/FrameworksSection'
+import { HomeContent } from './_components/HomeContent'
+
+export default function HomePage() {
+ return } />
+}
diff --git a/apps/www/app/blog/BlogHero.tsx b/apps/www/app/blog/BlogHero.tsx
index b3516542b70..4f63439d718 100644
--- a/apps/www/app/blog/BlogHero.tsx
+++ b/apps/www/app/blog/BlogHero.tsx
@@ -29,10 +29,10 @@ function SecondarySpotlight({ post }: { post: PostTypes }) {
+
{children}
diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx
deleted file mode 100644
index cde263e913f..00000000000
--- a/apps/www/components/Hero/Hero.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import SectionContainer from '~/components/Layouts/SectionContainer'
-import { useSendTelemetryEvent } from '~/lib/telemetry'
-import Link from 'next/link'
-import { Button } from 'ui'
-
-const Hero = () => {
- const sendTelemetryEvent = useSendTelemetryEvent()
-
- return (
-
-
-
-
-
-
-
-
- Build in a weekend
- Scale to millions
-
-
- Supabase is the Postgres development platform.{' '}
-
- Start your project with a Postgres database, Authentication, instant APIs, Edge
- Functions, Realtime subscriptions, Storage, and Vector embeddings.
-
-
-
-
-
- sendTelemetryEvent({
- action: 'start_project_button_clicked',
- properties: { buttonLocation: 'Homepage Hero' },
- })
- }
- >
- Start your project
-
-
-
-
- sendTelemetryEvent({
- action: 'request_demo_button_clicked',
- properties: { buttonLocation: 'Homepage Hero' },
- })
- }
- >
- Request a demo
-
-
-
-
-
-
-
-
-
- )
-}
-
-export default Hero
diff --git a/apps/www/components/Nav/GitHubButton.tsx b/apps/www/components/Nav/GitHubButton.tsx
index 392b440daf8..46c7f728a4e 100644
--- a/apps/www/components/Nav/GitHubButton.tsx
+++ b/apps/www/components/Nav/GitHubButton.tsx
@@ -2,27 +2,27 @@ import staticContent from '.generated/staticContent/_index.json'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { Button } from 'ui'
+export const kFormatter = (num: number) => {
+ const kFormat = Math.floor(num / 1000)
+ const lastTwoDigits = num % 1000
+
+ const decimalPart = Math.floor((lastTwoDigits % 100) / 10)
+ const hundreds = Math.floor(lastTwoDigits / 100)
+
+ const isAlmostNextThousand = decimalPart >= 8 && hundreds >= 9
+
+ const showDecimals =
+ (!isAlmostNextThousand && hundreds >= 1) || (hundreds === 0 && decimalPart >= 8)
+
+ return showDecimals
+ ? `${kFormat}.${decimalPart >= 8 ? hundreds + 1 : hundreds}K`
+ : `${isAlmostNextThousand ? kFormat + 1 : kFormat}K`
+}
+
const GitHubButton = () => {
const sendTelemetryEvent = useSendTelemetryEvent()
const githubStars = staticContent.githubStars
- const kFormatter = (num: number) => {
- const kFormat = Math.floor(num / 1000)
- const lastTwoDigits = num % 1000
-
- const decimalPart = Math.floor((lastTwoDigits % 100) / 10)
- const hundreds = Math.floor(lastTwoDigits / 100)
-
- const isAlmostNextThousand = decimalPart >= 8 && hundreds >= 9
-
- const showDecimals =
- (!isAlmostNextThousand && hundreds >= 1) || (hundreds === 0 && decimalPart >= 8)
-
- return showDecimals
- ? `${kFormat}.${decimalPart >= 8 ? hundreds + 1 : hundreds}K`
- : `${isAlmostNextThousand ? kFormat + 1 : kFormat}K`
- }
-
return (
{
- const sendTelemetryEvent = useSendTelemetryEvent()
-
- return (
-
- Open source from day one
-
- Supabase is built in the open because we believe great developer tools should be
- transparent, inspectable, and owned by the community. Read, contribute, self-host. You're
- never locked in, and always in control.
-
-
-
-
-
- }
- asChild
- size="small"
- variant="default"
- className="pl-2.5!"
- >
- sendTelemetryEvent({ action: 'homepage_github_button_clicked' })}
- >
- @supabase
- {staticContent.githubStars != null && (
- <>
-
- {kFormatter(staticContent.githubStars)}
- >
- )}
-
-
-
Top 100 GitHub repos
-
-
- )
-}
-
-export default OpenSourceSection
diff --git a/apps/www/components/Products/ProductCard.tsx b/apps/www/components/Products/ProductCard.tsx
index bb39b334ad0..25e5d86960e 100644
--- a/apps/www/components/Products/ProductCard.tsx
+++ b/apps/www/components/Products/ProductCard.tsx
@@ -1,8 +1,8 @@
-import React from 'react'
-import Link from 'next/link'
import Panel from '~/components/Panel'
-import { cn } from 'ui'
import { detectBrowser, isBrowser } from 'common'
+import Link from 'next/link'
+import React from 'react'
+import { cn } from 'ui'
const ProductCard = ({
className,
@@ -62,6 +62,7 @@ const ProductCard = ({
{icon && (
- {image && image}
+ {image && {image} }
)
diff --git a/apps/www/data/home/content.tsx b/apps/www/data/home/content.tsx
index bde1c25262f..43477e46503 100644
--- a/apps/www/data/home/content.tsx
+++ b/apps/www/data/home/content.tsx
@@ -1,12 +1,11 @@
import VideoWithHighlights from 'components/VideoWithHighlights'
+import MainProducts from 'data/MainProducts'
import { useSendTelemetryEvent } from 'lib/telemetry'
import Link from 'next/link'
-import { Button } from 'ui'
-import ProductModules from '../ProductModules'
-
-import MainProducts from 'data/MainProducts'
import { topTweets } from 'shared-data/tweets'
-import { IconDiscord } from 'ui'
+import { Button, IconDiscord } from 'ui'
+
+import ProductModules from '../ProductModules'
export default () => {
const sendTelemetryEvent = useSendTelemetryEvent()
@@ -49,10 +48,10 @@ export default () => {
icon: 'M4.13477 12.8129C4.13477 14.1481 4.43245 15.4138 4.96506 16.5471M12.925 4.02271C11.5644 4.02271 10.276 4.33184 9.12614 4.88371M21.7152 12.8129C21.7152 11.4644 21.4115 10.1867 20.8688 9.0447M12.925 21.6032C14.2829 21.6032 15.5689 21.2952 16.717 20.7454M16.717 20.7454C17.2587 21.5257 18.1612 22.0366 19.1831 22.0366C20.84 22.0366 22.1831 20.6935 22.1831 19.0366C22.1831 17.3798 20.84 16.0366 19.1831 16.0366C17.5263 16.0366 16.1831 17.3798 16.1831 19.0366C16.1831 19.6716 16.3804 20.2605 16.717 20.7454ZM4.96506 16.5471C4.16552 17.086 3.63965 17.9999 3.63965 19.0366C3.63965 20.6935 4.98279 22.0366 6.63965 22.0366C8.2965 22.0366 9.63965 20.6935 9.63965 19.0366C9.63965 17.3798 8.2965 16.0366 6.63965 16.0366C6.01951 16.0366 5.44333 16.2248 4.96506 16.5471ZM9.12614 4.88371C8.58687 4.08666 7.67444 3.56274 6.63965 3.56274C4.98279 3.56274 3.63965 4.90589 3.63965 6.56274C3.63965 8.2196 4.98279 9.56274 6.63965 9.56274C8.2965 9.56274 9.63965 8.2196 9.63965 6.56274C9.63965 5.94069 9.45032 5.36285 9.12614 4.88371ZM20.8688 9.0447C21.6621 8.50486 22.1831 7.59464 22.1831 6.56274C22.1831 4.90589 20.84 3.56274 19.1831 3.56274C17.5263 3.56274 16.1831 4.90589 16.1831 6.56274C16.1831 8.2196 17.5263 9.56274 19.1831 9.56274C19.8081 9.56274 20.3884 9.37165 20.8688 9.0447Z',
description: (
<>
- Instant ready-to-use Restful APIs .
+ Instant ready-to-use REST APIs .
>
),
- description_short: 'Instant ready-to-use Restful APIs.',
+ description_short: 'Instant ready-to-use REST APIs.',
label: '',
url: 'https://supabase.com/docs/guides/api',
},
diff --git a/apps/www/data/surveys/state-of-startups-2026.tsx b/apps/www/data/surveys/state-of-startups-2026.tsx
index f48f8b6eb6a..87182034cba 100644
--- a/apps/www/data/surveys/state-of-startups-2026.tsx
+++ b/apps/www/data/surveys/state-of-startups-2026.tsx
@@ -345,7 +345,7 @@ const stateOfStartupsData = {
eyebrow: 'Older is the new younger',
title: 'Founders 40 and older grew from 18% to 25%.',
description:
- 'Every age band above 40 grew by a statistically significant margin. The 22 to 29 cohort shrank by 5%. Seasoned operators are filling the gap, often with AI doing the typing.',
+ 'Every age band above 40 grew by a statistically significant margin. The 22 to 29 cohort shrank by 4%. Seasoned operators are filling the gap, often with AI doing the typing.',
pullQuote: {
quote: 'It has allowed me to build the vision that has been in my head for decades',
author: 'Mazz Ink',
@@ -362,7 +362,7 @@ const stateOfStartupsData = {
eyebrow: 'From anywhere',
title: 'You can build a great company from anywhere now.',
description:
- 'The top metros are still the top metros. But AI has flattened the development gap everywhere else. Europe grew 4%; Africa grew 2%. Startups are setting up across Toronto, Chicago, Denver, and Austin, and throughout Europe, Asia, and Africa.',
+ 'The top metros are still the top metros. But AI has flattened the development gap everywhere else. Europe and Africa grew 3%. Startups are setting up across Toronto, Chicago, Denver, and Austin, and throughout Europe, Asia, and Africa.',
pullQuote: {
quote:
'Speed of iteration is mind blowing. Even non-technical people can now translate creativity and ideas into tangible assets.',
@@ -486,7 +486,7 @@ const stateOfStartupsData = {
eyebrow: 'Model providers',
title: 'Anthropic is the default model provider.',
description:
- 'Anthropic/Claude climbed from 38% to 64%; OpenAI fell from 69% to 51%. Gemini entered at 44%. Hugging Face and custom models lost material share, a sign fewer teams run their own inference.',
+ 'Anthropic/Claude climbed from 38% to 64%; OpenAI fell from 69% to 52%. Gemini entered at 44%. Hugging Face and custom models lost material share, a sign fewer teams run their own inference.',
stats: [
stat('use Anthropic models', 'ai_models_used', 'multi', 'Anthropic/Claude'),
stat('use OpenAI models', 'ai_models_used', 'multi', 'OpenAI'),
@@ -507,7 +507,7 @@ const stateOfStartupsData = {
shortTitle: 'AI-written code',
title: 'AI-generated code is the median experience.',
description:
- '62% of startups have more than half their codebase written by AI. 40% are at 76 to 100%. Only 2% are at zero. Older founders use it more heavily than younger ones, and non-technical founders more than technical ones.',
+ '61% of startups have more than half their codebase written by AI. 40% are at 76 to 100%. Only 2% are at zero. Older founders use it more heavily than younger ones, and non-technical founders more than technical ones.',
sections: [
{
id: 'ai-codebase',
diff --git a/apps/www/fonts/index.ts b/apps/www/fonts/index.ts
new file mode 100644
index 00000000000..0d6f155e30a
--- /dev/null
+++ b/apps/www/fonts/index.ts
@@ -0,0 +1,22 @@
+import { Inter, Manrope, Source_Code_Pro } from 'next/font/google'
+
+export const manrope = Manrope({
+ variable: '--font-manrope',
+ display: 'swap',
+ fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
+ subsets: ['latin'],
+})
+
+export const inter = Inter({
+ variable: '--font-inter',
+ display: 'swap',
+ fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'],
+ subsets: ['latin'],
+})
+
+export const sourceCodePro = Source_Code_Pro({
+ variable: '--font-source-code-pro',
+ display: 'swap',
+ fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'],
+ subsets: ['latin'],
+})
diff --git a/apps/www/lib/fonts.ts b/apps/www/lib/fonts.ts
new file mode 100644
index 00000000000..5e75a726c09
--- /dev/null
+++ b/apps/www/lib/fonts.ts
@@ -0,0 +1 @@
+export { inter, manrope, sourceCodePro } from '~/fonts'
diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx
index bb274f0ddb0..6c31fa3419d 100644
--- a/apps/www/pages/_app.tsx
+++ b/apps/www/pages/_app.tsx
@@ -3,6 +3,7 @@ import 'config/code-hike.css'
import '../styles/globals.css'
import './launch-week/launchWeek.css'
+import { inter, manrope, sourceCodePro } from '~/lib/fonts'
import {
AuthProvider,
FeatureFlagProvider,
@@ -101,29 +102,31 @@ export default function App({ Component, pageProps }: AppProps) {
}}
/>
-
- {/* [TODO] I think we need to deconflict with the providers in layout.tsx? */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {/* [TODO] I think we need to deconflict with the providers in layout.tsx? */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
)
}
diff --git a/apps/www/pages/index.tsx b/apps/www/pages/index.tsx
deleted file mode 100644
index d5eb5a5de84..00000000000
--- a/apps/www/pages/index.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import Hero from '~/components/Hero/Hero'
-import Layout from '~/components/Layouts/Default'
-import Logos from '~/components/logos'
-import getContent from '~/data/home/content'
-import { organizationSchema, serializeJsonLd, websiteSchema } from '~/lib/json-ld'
-import { NextSeo } from 'next-seo'
-import dynamic from 'next/dynamic'
-import Head from 'next/head'
-
-const Products = dynamic(() => import('~/components/Products/index'))
-const HeroFrameworks = dynamic(() => import('~/components/Hero/HeroFrameworks'))
-const CustomerStories = dynamic(() => import('components/CustomerStories'))
-const BuiltWithSupabase = dynamic(() => import('components/BuiltWithSupabase'))
-const DashboardFeatures = dynamic(() => import('~/components/DashboardFeatures'))
-const TwitterSocialSection = dynamic(() => import('~/components/TwitterSocialSection'))
-const OpenSourceSection = dynamic(() => import('~/components/OpenSourceSection'))
-const CTABanner = dynamic(() => import('components/CTABanner/index'))
-
-// When updating page content, also update public/llms/homepage.txt
-
-const HOMEPAGE_JSON_LD = serializeJsonLd([organizationSchema(), websiteSchema()])
-
-const Index = () => {
- const content = getContent()
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )
-}
-
-export default Index
diff --git a/apps/www/public/images/customers/logos/chatbase-icon.svg b/apps/www/public/images/customers/logos/chatbase-icon.svg
new file mode 100644
index 00000000000..e772486c2a6
--- /dev/null
+++ b/apps/www/public/images/customers/logos/chatbase-icon.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/apps/www/public/images/customers/logos/e2b-icon.svg b/apps/www/public/images/customers/logos/e2b-icon.svg
new file mode 100644
index 00000000000..13ef7282fe1
--- /dev/null
+++ b/apps/www/public/images/customers/logos/e2b-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/apps/www/public/images/customers/logos/firecrawl-icon.svg b/apps/www/public/images/customers/logos/firecrawl-icon.svg
new file mode 100644
index 00000000000..34dba9b7ad1
--- /dev/null
+++ b/apps/www/public/images/customers/logos/firecrawl-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/apps/www/public/images/customers/logos/hyper-icon.svg b/apps/www/public/images/customers/logos/hyper-icon.svg
new file mode 100644
index 00000000000..790af453eec
--- /dev/null
+++ b/apps/www/public/images/customers/logos/hyper-icon.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/apps/www/public/images/customers/logos/markprompt-icon.svg b/apps/www/public/images/customers/logos/markprompt-icon.svg
new file mode 100644
index 00000000000..0e288cb1d8e
--- /dev/null
+++ b/apps/www/public/images/customers/logos/markprompt-icon.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/apps/www/public/images/customers/logos/mobbin-icon.svg b/apps/www/public/images/customers/logos/mobbin-icon.svg
new file mode 100644
index 00000000000..93d694b32d4
--- /dev/null
+++ b/apps/www/public/images/customers/logos/mobbin-icon.svg
@@ -0,0 +1 @@
+
diff --git a/apps/www/public/images/customers/logos/rally-icon.svg b/apps/www/public/images/customers/logos/rally-icon.svg
new file mode 100644
index 00000000000..3d1da318649
--- /dev/null
+++ b/apps/www/public/images/customers/logos/rally-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/apps/www/public/images/customers/logos/resend-icon.svg b/apps/www/public/images/customers/logos/resend-icon.svg
new file mode 100644
index 00000000000..b590c3695fd
--- /dev/null
+++ b/apps/www/public/images/customers/logos/resend-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/apps/www/styles/globals.css b/apps/www/styles/globals.css
index b5a771a962d..ba5b4e9125f 100644
--- a/apps/www/styles/globals.css
+++ b/apps/www/styles/globals.css
@@ -12,6 +12,13 @@
@source './../../../packages/ui-patterns/src/**/*.{tsx,ts,js}';
@source './../../../packages/marketing/src/**/*.{tsx,ts,js}';
+@theme inline {
+ --font-sans:
+ var(--font-inter), Inter, Helvetica Neue, Helvetica, ui-sans-serif, system-ui, sans-serif;
+ --font-heading: var(--font-manrope, var(--font-sans));
+ --font-mono: var(--font-source-code-pro), 'Source Code Pro', ui-monospace, Menlo, monospace;
+}
+
@utility section-container {
@apply w-full mx-auto max-w-7xl px-6 lg:px-12 xl:px-24;
}
@@ -37,6 +44,21 @@
--delay-1200: 1200ms;
--delay-1500: 1500ms;
+
+ /* font sizing and weights optimized for Inter */
+ --text-sm: 0.8125rem;
+ --text-base: 0.9375rem;
+ --text-lg: 1rem;
+ --text-xl: 1.125rem;
+ --text-2xl: 1.375rem;
+ --text-3xl: 1.75rem;
+ --text-4xl: 2.125rem;
+ --text-5xl: 2.875rem;
+ --text-6xl: 3.625rem;
+ --text-7xl: 4.375rem;
+ --text-8xl: 5.875rem;
+ --text-9xl: 7.875rem;
+ --font-weight-normal: 450;
}
@keyframes flash-code {
@@ -115,6 +137,21 @@
::file-selector-button {
border-color: hsl(var(--border-default, currentColor));
}
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ .h1,
+ .h2,
+ .h3,
+ .h4,
+ .h5,
+ .h6 {
+ @apply font-heading font-semibold;
+ }
}
@layer utilities {
@@ -132,28 +169,9 @@
}
}
-@font-face {
- font-family: 'custom-font';
- src:
- url(../../../packages/common/assets/fonts/CustomFont-Book.woff2) format('woff2'),
- url(../../../packages/common/assets/fonts/CustomFont-Book.woff) format('woff');
- font-weight: 400;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'custom-font';
- src:
- url(../../../packages/common/assets/fonts/CustomFont-Medium.woff2) format('woff2'),
- url(../../../packages/common/assets/fonts/CustomFont-Medium.woff) format('woff');
- font-weight: 500;
- font-style: normal;
- font-display: swap;
-}
-
html {
scroll-behavior: smooth !important;
+ font-weight: 450;
}
body {
diff --git a/apps/www/styles/realtime.module.css b/apps/www/styles/realtime.module.css
index a58d8edb4ea..7ec78abf4a9 100644
--- a/apps/www/styles/realtime.module.css
+++ b/apps/www/styles/realtime.module.css
@@ -104,7 +104,7 @@
88.1%,
100% {
opacity: 1;
- font-family: 'custom-font';
+ font-family: var(--font-sans);
background-color: transparent;
}
}
diff --git a/docker/.env.example b/docker/.env.example
index 417e384fbf7..c944f572097 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -58,23 +58,34 @@ JWT_JWKS=
DASHBOARD_USERNAME=supabase
DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated
-# Used by Realtime and Supavisor
+# Encryption key for securing Realtime and Supavisor communications.
+# (Must be at least 64 characters; generate with: openssl rand -base64 48)
SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
-# Used by Supavisor
+# Encryption key used by Realtime for sensitive fields in the `_realtime` schema.
+# (Must be exactly 16 characters; generate with: `openssl rand -hex 8`)
+REALTIME_DB_ENC_KEY=supabaserealtime
+
+# Encryption key used by Supavisor for storing encrypted configuration.
+# (Must be exactly 32 characters; generate with: openssl rand -hex 16)
VAULT_ENC_KEY=your-32-character-encryption-key
-# Used by Studio to access Postgres via postgres-meta
+# Encryption key for securing connection strings used by Studio against postgres-meta.
+# (Must be at least 32 characters; generate with openssl rand -base64 24)
PG_META_CRYPTO_KEY=your-encryption-key-32-chars-min
-# Analytics - API tokens for log ingestion/querying, and for management
-# If Logflare has to be externally exposed - configure securely!
-# Used in the docker-compose.logs.yml override.
+# API token for log ingestion used by Logflare and Vector.
+# (Must be at least 32 characters; generate with openssl rand -base64 24)
LOGFLARE_PUBLIC_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-public
+# API token used for Logflare management operations. Never expose client-side.
+# (Must be at least 32 characters; generate with openssl rand -base64 24)
LOGFLARE_PRIVATE_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-private
-# Access to Storage via S3 protocol endpoint (see below)
+# Access key ID (username-like) for accessing the S3 protocol endpoint in Storage.
+# (Generate with: openssl rand -hex 16)
S3_PROTOCOL_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c
+# Secret key (password-like) used with S3_PROTOCOL_ACCESS_KEY_ID.
+# (Generate with: openssl rand -hex 32)
S3_PROTOCOL_ACCESS_KEY_SECRET=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
@@ -269,6 +280,8 @@ REGION=stub
# Used by MinIO when added via:
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
MINIO_ROOT_USER=supa-storage
+# Root administrator password for the RustFS or MinIO server.
+# (Must be 8+ characters; generate with: openssl rand -hex 16)
MINIO_ROOT_PASSWORD=secret1234
# Equivalent to project_ref as described here:
diff --git a/docker/CONFIG.md b/docker/CONFIG.md
index ae5ed04fd04..50cb34b88b5 100644
--- a/docker/CONFIG.md
+++ b/docker/CONFIG.md
@@ -126,7 +126,6 @@ These mirror the running PostgREST configuration so the dashboard can display co
|---|---|---|---|---|
| `LOGFLARE_API_KEY` | string | Self-hosted | Legacy alias for `LOGFLARE_PUBLIC_ACCESS_TOKEN`. | Deprecated. Declared in `apps/studio/turbo.jsonc` but not read by Studio code; kept only for backward compatibility with older deployments. |
| `LOGFLARE_PRIVATE_ACCESS_TOKEN` | string | Both | Private API token Studio uses server-side to query Logflare endpoints (logs, charts). | Required for logs/analytics features to work on self-hosted. |
-| `LOGFLARE_PUBLIC_ACCESS_TOKEN` | string | Self-hosted | Public API token used by the analytics (supabase/logflare) container for ingestion. | Not read by Studio code (despite being in `apps/studio/turbo.jsonc`). Passed through the `studio` service env in `docker-compose.yml` for parity only. |
| `LOGFLARE_URL` | URL | Both | Base URL of the Logflare/analytics service. | E.g. `http://analytics:4000`. Used to build the `PROJECT_ANALYTICS_URL`. |
| `NEXT_ANALYTICS_BACKEND_PROVIDER` | enum | Both | Historically intended to select the analytics container's backend (`postgres` or `bigquery`). | No-op today: not read by Studio code, and the `analytics` (supabase/logflare) container chooses its backend via `POSTGRES_BACKEND_URL` / `LOGFLARE_FEATURE_FLAG_OVERRIDE` instead. Safe to ignore. |
| `NEXT_PUBLIC_ENABLE_LOGS` | boolean | Both | Historically intended to toggle visibility of log explorer pages. | Not read by Studio code today, and not declared in `apps/studio/turbo.jsonc`. Use `ENABLED_FEATURES_LOGS_ALL` (see Feature flags below) for runtime control of the logs section. |
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 82eb4102bb4..db9ab04a4f8 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -4,8 +4,8 @@
# Dev mode: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up -d
# Reset everything: sh reset.sh
#
-# TODO:
-# - Podman does not support nested variable interpolation (${A:-${B}})
+# Notes:
+# - Nested variable interpolation (${A:-${B}}) requires podman-compose >= 1.6.0
#
name: supabase
@@ -312,7 +312,7 @@ services:
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: ${POSTGRES_DB}
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
- DB_ENC_KEY: supabaserealtime
+ DB_ENC_KEY: ${REALTIME_DB_ENC_KEY:-supabaserealtime}
# Legacy symmetric HS256 key
API_JWT_SECRET: ${JWT_SECRET}
diff --git a/docker/tests/test-pg17-upgrade.sh b/docker/tests/test-pg17-upgrade.sh
index 6ac7ff9f091..a4e2bfa45ea 100644
--- a/docker/tests/test-pg17-upgrade.sh
+++ b/docker/tests/test-pg17-upgrade.sh
@@ -112,6 +112,37 @@ pre_checksum=$(run_sql -A -t -c "SELECT md5(string_agg(name || value::text, ','
echo " Rows: $pre_count"
echo " Checksum: $pre_checksum"
+# --- Seed a Vault secret ---------------------------------------------------
+# Verifies the pgsodium root key survives the volume swap/chown AND that Vault
+# secrets still decrypt on Postgres 17. For legacy (key_id-based) secrets this
+# also exercises complete.sh's pgsodium->Vault re-encryption; on a stock
+# self-hosted stack the secret is already pgsodium-less, so this confirms the
+# round-trip and the post-upgrade invariant (key_id IS NULL).
+VAULT_SECRET_NAME="upgrade_test_secret"
+VAULT_SECRET_VALUE="upgrade-test-secret-value-42"
+vault_available="f"
+if [ "$(run_sql -A -t -c "SELECT EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'supabase_vault');" | tr -d '[:space:]')" = "t" ]; then
+ echo ""
+ echo "Seeding Vault secret on Postgres 15..."
+ run_sql <&2
+ fi
+ vault_available="t"
+else
+ echo ""
+ echo "Skipping Vault seed: supabase_vault extension not available."
+fi
+
# --- Run upgrade -----------------------------------------------------------
echo ""
@@ -127,11 +158,37 @@ echo ""
echo "Running pgTAP verification..."
echo ""
-# Use a non-quoted heredoc so $pre_count and $pre_checksum are interpolated
+# Optional Vault assertions, only when a secret was seeded above.
+vault_plan=0
+vault_tests=""
+if [ "$vault_available" = "t" ]; then
+ vault_plan=3
+ vault_tests=$(cat < void
+ devToolbar?: () => void
}
}
+const flags = vi.hoisted(() => ({ devToolbarDefaultOn: false }))
+
// Mock common package
vi.mock('common', async () => {
return {
@@ -16,6 +18,7 @@ vi.mock('common', async () => {
posthog: {},
configcat: {},
}),
+ useFlag: (name: string) => (name === 'devToolbarDefaultOn' ? flags.devToolbarDefaultOn : false),
posthogClient: {
subscribeToEvents: vi.fn(() => () => {}),
},
@@ -27,7 +30,7 @@ const originalEnv = process.env.NEXT_PUBLIC_ENVIRONMENT
/**
* Helper to render the full component tree as used in production.
- * The Provider sets up window.devTelemetry and manages state.
+ * The Provider sets up window.devToolbar and manages state.
* The Trigger shows the activity icon in the header.
* The Toolbar is the actual panel/sheet.
*/
@@ -73,7 +76,8 @@ describe('DevToolbar', () => {
})
localStorage.clear()
- delete window.devTelemetry
+ flags.devToolbarDefaultOn = false
+ delete window.devToolbar
vi.spyOn(console, 'warn').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
})
@@ -101,11 +105,11 @@ describe('DevToolbar', () => {
expect(container.querySelector('button')).toBeNull()
})
- it('does not register window.devTelemetry', async () => {
+ it('does not register window.devToolbar', async () => {
vi.resetModules()
await renderFullToolbar()
- expect(window.devTelemetry).toBeUndefined()
+ expect(window.devToolbar).toBeUndefined()
})
})
@@ -122,12 +126,12 @@ describe('DevToolbar', () => {
expect(container.querySelector('button')).toBeNull()
})
- it('registers window.devTelemetry function', async () => {
+ it('registers window.devToolbar function', async () => {
vi.resetModules()
await renderFullToolbar()
- expect(window.devTelemetry).toBeDefined()
- expect(typeof window.devTelemetry).toBe('function')
+ expect(window.devToolbar).toBeDefined()
+ expect(typeof window.devToolbar).toBe('function')
})
})
@@ -218,7 +222,41 @@ describe('DevToolbar', () => {
})
})
- describe('window.devTelemetry function', () => {
+ describe('when devToolbarDefaultOn flag is enabled', () => {
+ it('renders the trigger without calling window.devToolbar in local', async () => {
+ process.env.NEXT_PUBLIC_ENVIRONMENT = 'local'
+ flags.devToolbarDefaultOn = true
+
+ vi.resetModules()
+ await renderFullToolbar()
+
+ const triggerButton = screen.getByRole('button')
+ expect(triggerButton).toBeInTheDocument()
+ })
+
+ it('renders the trigger without calling window.devToolbar in staging', async () => {
+ process.env.NEXT_PUBLIC_ENVIRONMENT = 'staging'
+ flags.devToolbarDefaultOn = true
+
+ vi.resetModules()
+ await renderFullToolbar()
+
+ const triggerButton = screen.getByRole('button')
+ expect(triggerButton).toBeInTheDocument()
+ })
+
+ it('does not render the trigger in production even when flag is on', async () => {
+ process.env.NEXT_PUBLIC_ENVIRONMENT = 'prod'
+ flags.devToolbarDefaultOn = true
+
+ vi.resetModules()
+ const { container } = await renderFullToolbar()
+
+ expect(container.querySelector('button')).toBeNull()
+ })
+ })
+
+ describe('window.devToolbar function', () => {
beforeEach(() => {
process.env.NEXT_PUBLIC_ENVIRONMENT = 'local'
})
@@ -230,9 +268,9 @@ describe('DevToolbar', () => {
// Trigger should not be visible initially
expect(screen.queryByRole('button')).not.toBeInTheDocument()
- // Call devTelemetry to enable
+ // Call devToolbar to enable
act(() => {
- window.devTelemetry?.()
+ window.devToolbar?.()
})
// Re-import and rerender to pick up state change
@@ -256,17 +294,17 @@ describe('DevToolbar', () => {
})
describe('cleanup', () => {
- it('removes window.devTelemetry on unmount', async () => {
+ it('removes window.devToolbar on unmount', async () => {
process.env.NEXT_PUBLIC_ENVIRONMENT = 'local'
vi.resetModules()
const result = await renderFullToolbar()
- expect(window.devTelemetry).toBeDefined()
+ expect(window.devToolbar).toBeDefined()
result.unmount()
- expect(window.devTelemetry).toBeUndefined()
+ expect(window.devToolbar).toBeUndefined()
})
})
diff --git a/packages/dev-tools/DevToolbarContext.tsx b/packages/dev-tools/DevToolbarContext.tsx
index 0c46a1d0323..6ba2c7cf643 100644
--- a/packages/dev-tools/DevToolbarContext.tsx
+++ b/packages/dev-tools/DevToolbarContext.tsx
@@ -1,6 +1,6 @@
'use client'
-import { ensurePlatformSuffix, posthogClient, type ClientTelemetryEvent } from 'common'
+import { ensurePlatformSuffix, posthogClient, useFlag, type ClientTelemetryEvent } from 'common'
import {
createContext,
useCallback,
@@ -32,7 +32,7 @@ const SSE_BACKOFF_MULTIPLIER = 2
declare global {
interface Window {
- devTelemetry?: () => void
+ devToolbar?: () => void
}
}
@@ -48,6 +48,8 @@ export function DevToolbarProvider({ children, apiUrl }: DevToolbarProviderProps
const [isOpen, setIsOpen] = useState(false)
const [events, setEvents] = useState([])
+ const isDefaultOn = useFlag('devToolbarDefaultOn')
+
const sseRetryDelayRef = useRef(SSE_INITIAL_RETRY_MS)
const sseRetryTimeoutRef = useRef(null)
@@ -66,11 +68,11 @@ export function DevToolbarProvider({ children, apiUrl }: DevToolbarProviderProps
try {
stored = localStorage.getItem(STORAGE_KEY)
} catch {}
- if (stored === 'true') {
+ if (stored === 'true' || isDefaultOn) {
setIsEnabled(true)
}
- window.devTelemetry = () => {
+ window.devToolbar = () => {
try {
localStorage.setItem(STORAGE_KEY, 'true')
} catch {}
@@ -78,9 +80,9 @@ export function DevToolbarProvider({ children, apiUrl }: DevToolbarProviderProps
}
return () => {
- delete window.devTelemetry
+ delete window.devToolbar
}
- }, [])
+ }, [isDefaultOn])
const appendEvent = useCallback((event: DevTelemetryEvent) => {
setEvents((prev) => {