From ec260a594d68a46ac9e0762562fe5a16cc3d92ff Mon Sep 17 00:00:00 2001 From: Kanishk Dudeja Date: Thu, 23 Jul 2026 01:33:36 +0530 Subject: [PATCH 001/141] docs(billing): clarify top-ups do not apply to outstanding invoices (#48220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This PR clarifies that credit top-ups apply only to future invoices and cannot be used to pay or adjust outstanding invoices. It updates the Credits FAQ, Billing FAQ, and credit top-up modal with consistent wording. ### Testing #### Credits FAQ https://docs-git-kanishk-billing-2726-update-billing-fa-bd77f2-supabase.vercel.app/docs/guides/platform/credits#credit-faq #### Billing FAQ https://docs-git-kanishk-billing-2726-update-billing-fa-bd77f2-supabase.vercel.app/docs/guides/platform/billing-faq#payments-and-billing-cycle #### Credit Top Up Modal Screenshot 2026-07-23 at 1 17 30 AM ## Summary by CodeRabbit * **Documentation** * Added FAQ guidance explaining that credit top-ups apply only to future invoices and cannot pay or adjust outstanding invoices. * **Billing Updates** * Clarified that credits are granted based on the pre-tax payment amount. * Confirmed that credits are non-refundable and do not expire. --- apps/docs/content/guides/platform/billing-faq.mdx | 4 ++++ apps/docs/content/guides/platform/credits.mdx | 4 ++++ .../interfaces/Organization/BillingSettings/CreditTopUp.tsx | 5 +++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 83906fe29cd..5c4941ac7f4 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -208,6 +208,10 @@ Note that any changes made to your billing details will only be reflected in you When an invoice becomes overdue, we will pause your projects and downgrade your organization to the Free Plan. You will be able to restore your projects once you have paid all outstanding invoices. +#### Can I use a credit top-up to pay an outstanding invoice? + +Credit top-ups apply only to future invoices. They cannot be used to pay or adjust outstanding invoices. + #### Why am I overdue? We were unable to charge your payment method. This likely means that the payment was not successfully processed with the credit card on your account profile. diff --git a/apps/docs/content/guides/platform/credits.mdx b/apps/docs/content/guides/platform/credits.mdx index cf3b30a36b1..7786982481b 100644 --- a/apps/docs/content/guides/platform/credits.mdx +++ b/apps/docs/content/guides/platform/credits.mdx @@ -67,6 +67,10 @@ height={758} Yes, once the payment is confirmed, you will get a matching invoice that can be accessed through your [organization's invoices page](/dashboard/org/_/billing#invoices). +### Can I use a credit top-up to pay an outstanding invoice? + +Credit top-ups apply only to future invoices. They cannot be used to pay or adjust outstanding invoices. + ### Can I transfer credits to another organization? Yes, you can transfer credits to another organization. Submit a [support ticket](https://supabase.help). diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx index 25feaef32e1..b08a50d3d00 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditTopUp.tsx @@ -301,8 +301,9 @@ export const CreditTopUp = ({ slug }: { slug: string | undefined }) => {

On successful payment, an invoice will be issued and you'll be granted credits equal - to the pre-tax amount. Credits will be applied to future invoices only and are not - refundable. The topped up credits do not expire. + to the pre-tax amount. Credits will be applied to future invoices only. They cannot be + used to pay or adjust outstanding invoices. Credits are non-refundable and do not + expire.

For larger discounted credit packages, please reach out to us via{' '} From e4f75bf74cf827bc8a83d5c5b5f3136314269bba Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 22 Jul 2026 22:22:54 +0200 Subject: [PATCH 002/141] chore(studio): update copywriting on integrations pages (#48197) --- .../FeaturePreview/IntegrationsLayoutPreview.tsx | 3 +-- .../Organization/OAuthApps/RevokeAppModal.tsx | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx index ad21bc73acf..28c8e453fd1 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx @@ -4,8 +4,7 @@ import { BASE_PATH } from 'ui-patterns/CommandMenu/prepackaged/shared/constants' export const IntegrationsLayoutPreview = () => (

- Install Dashboard Integrations in a single click, with improved filtering and search to help - you find the one you need. + Install Dashboard Integrations in a single click and try the new layout.

  • The application may also have a Secret API key with access. - Go to the{' '} - - Integration's Settings - - , and remove any listed Secret API key to fully revoke its access. + Navigate to{' '} + + Integrations + {' '} + on the project this app was installed, and remove any listed Secret API key + in the "Settings" tab of the integration to fully revoke its access.
  • From 0bef8e7d90345838566fbbc9c083897322f2e671 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:55:17 -0400 Subject: [PATCH 003/141] test(sql-editor): e2e coverage + delete jsdom test + merge Results.utils tests (Steps 5-6) (#48217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Steps 5 and 6 of the SQL editor test refactor plan (the final two steps). **Step 5** — extends `e2e/studio/features/sql-editor.spec.ts` (real browser, zero mocks) with cases that need the real Monaco editor / full app render: - destructive-query warning modal: confirm actually re-runs the forced query (previously only `Cancel` was exercised) - debug button opens the AI Assistant with the query error pre-filled Deletes `apps/studio/tests/components/SQLEditor/SQLEditor.test.tsx` — its logic-level cases are now covered mock-free by the Step 4 hook tests, and its integration cases by e2e. Deleting rather than narrowing is the honest consequence of "no mocking": every remaining assertion it could make in jsdom requires a Monaco mock. **Step 6** — merges `apps/studio/tests/components/SQLEditor/Results.utils.test.ts` (`formatClipboardValue`/`formatCellValue`) into the colocated `apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.utils.test.ts` (`formatResults`/`convertResultsToMarkdown`/`convertResultsToJSON`/`getResultsHeaders`/`isLargeValue`/`convertResultsToCSV`) — both tested disjoint exports of the same source file. Deletes the `tests/` copy. This is the last step in the plan. ## Test plan - [x] `pnpm --filter studio typecheck` — no new errors in changed files - [x] `npx prettier --check` on all changed files - [x] Ran the new/changed e2e cases locally end-to-end against a live local stack — both pass - [x] `cd apps/studio && npx vitest run components/interfaces/SQLEditor/UtilityPanel/Results.utils.test.ts` — 42/42 passing after the merge ## Summary by CodeRabbit * **Tests** * Added end-to-end coverage for destructive SQL query warning modal flow before forced execution. * Added end-to-end coverage for the AI Assistant debug flow when SQL execution fails. * Expanded unit test coverage for SQL editor results formatting utilities (clipboard and cell value formatting). * Removed the prior SQLEditor unit test suite and the older results-formatting unit tests. --- .../UtilityPanel/Results.utils.test.ts | 44 ++ .../SQLEditor/Results.utils.test.ts | 48 -- .../components/SQLEditor/SQLEditor.test.tsx | 494 ------------------ e2e/studio/features/sql-editor.spec.ts | 78 +++ 4 files changed, 122 insertions(+), 542 deletions(-) delete mode 100644 apps/studio/tests/components/SQLEditor/Results.utils.test.ts delete mode 100644 apps/studio/tests/components/SQLEditor/SQLEditor.test.tsx diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.utils.test.ts b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.utils.test.ts index 020a9667b88..6006cde9feb 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.utils.test.ts +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.utils.test.ts @@ -4,12 +4,56 @@ import { convertResultsToCSV, convertResultsToJSON, convertResultsToMarkdown, + formatCellValue, + formatClipboardValue, formatResults, getResultsHeaders, isLargeValue, } from './Results.utils' describe('Results.utils', () => { + describe('formatClipboardValue', () => { + it('returns empty string for null', () => { + expect(formatClipboardValue(null)).toBe('') + }) + + it('stringifies objects', () => { + expect(formatClipboardValue({ a: 1 })).toBe('{"a":1}') + }) + + it('stringifies arrays', () => { + expect(formatClipboardValue([1, 2])).toBe('[1,2]') + }) + + it('converts primitives to string', () => { + expect(formatClipboardValue('hello')).toBe('hello') + expect(formatClipboardValue(42)).toBe('42') + expect(formatClipboardValue(false)).toBe('false') + }) + }) + + describe('formatCellValue', () => { + it('returns NULL for null', () => { + expect(formatCellValue(null)).toBe('NULL') + }) + + it('returns strings as-is', () => { + expect(formatCellValue('hello')).toBe('hello') + }) + + it('stringifies objects', () => { + expect(formatCellValue({ a: 1 })).toBe('{"a":1}') + }) + + it('stringifies numbers', () => { + expect(formatCellValue(42)).toBe('42') + }) + + it('stringifies booleans', () => { + expect(formatCellValue(true)).toBe('true') + }) + }) + describe('formatResults', () => { it('should stringify object values', () => { const results = [{ id: 1, data: { nested: true } }] diff --git a/apps/studio/tests/components/SQLEditor/Results.utils.test.ts b/apps/studio/tests/components/SQLEditor/Results.utils.test.ts deleted file mode 100644 index 1b2e0300ed6..00000000000 --- a/apps/studio/tests/components/SQLEditor/Results.utils.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { - formatCellValue, - formatClipboardValue, -} from '@/components/interfaces/SQLEditor/UtilityPanel/Results.utils' - -describe('formatClipboardValue', () => { - test('returns empty string for null', () => { - expect(formatClipboardValue(null)).toBe('') - }) - - test('stringifies objects', () => { - expect(formatClipboardValue({ a: 1 })).toBe('{"a":1}') - }) - - test('stringifies arrays', () => { - expect(formatClipboardValue([1, 2])).toBe('[1,2]') - }) - - test('converts primitives to string', () => { - expect(formatClipboardValue('hello')).toBe('hello') - expect(formatClipboardValue(42)).toBe('42') - expect(formatClipboardValue(false)).toBe('false') - }) -}) - -describe('formatCellValue', () => { - test('returns NULL for null', () => { - expect(formatCellValue(null)).toBe('NULL') - }) - - test('returns strings as-is', () => { - expect(formatCellValue('hello')).toBe('hello') - }) - - test('stringifies objects', () => { - expect(formatCellValue({ a: 1 })).toBe('{"a":1}') - }) - - test('stringifies numbers', () => { - expect(formatCellValue(42)).toBe('42') - }) - - test('stringifies booleans', () => { - expect(formatCellValue(true)).toBe('true') - }) -}) diff --git a/apps/studio/tests/components/SQLEditor/SQLEditor.test.tsx b/apps/studio/tests/components/SQLEditor/SQLEditor.test.tsx deleted file mode 100644 index 7d828dbf9f2..00000000000 --- a/apps/studio/tests/components/SQLEditor/SQLEditor.test.tsx +++ /dev/null @@ -1,494 +0,0 @@ -import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react' -import { HttpResponse } from 'msw' -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' - -import { SQLEditor } from '@/components/interfaces/SQLEditor/SQLEditor' -import { DiffType } from '@/components/interfaces/SQLEditor/SQLEditor.types' -import { sqlEditorDiffRequestState } from '@/state/sql-editor/sql-editor-diff-request' -import { sqlEditorSessionState } from '@/state/sql-editor/sql-editor-session-state' -import { sqlEditorState } from '@/state/sql-editor/sql-editor-state' -import { customRender } from '@/tests/lib/custom-render' -import { addAPIMock } from '@/tests/lib/msw' - -/** - * Characterization tests for the (pre-decomposition) `SQLEditor` monolith. - * - * These tests are not intended to be a model of best practices; they - * over-mock and assert on implementation details. They are temporary to - * capture regressions while the editor is being decomposed into smaller, more - * testable components. - */ - -const SNIPPET_ID = 'test-snippet-id' - -// A single mutable fake-editor state so tests can drive selection / value / -// decoration ids, shared into the hoisted module mocks below. -const mocks = vi.hoisted(() => { - const state = { - value: 'select 1;', - selection: null as null | { startLineNumber: number }, - selectionValue: undefined as string | undefined, - decorations: ['decoration-1'] as string[], - scrollHandler: null as null | ((e: { scrollTop: number }) => void), - diffModifiedValue: 'select 2;', - } - - const editor = { - getValue: () => state.value, - getSelection: () => state.selection, - getModel: () => ({ - getValueInRange: (_selection: unknown) => state.selectionValue, - getFullModelRange: () => ({ __fullRange: true }), - }), - executeEdits: vi.fn(), - deltaDecorations: vi.fn( - (_oldDecorations: string[], _newDecorations: any[]) => state.decorations - ), - revealLineInCenter: vi.fn(), - onDidScrollChange: vi.fn((cb: (e: { scrollTop: number }) => void) => { - state.scrollHandler = cb - }), - setScrollTop: vi.fn(), - focus: vi.fn(), - } - - class FakeRange { - startLineNumber: number - startColumn: number - endLineNumber: number - endColumn: number - constructor(a: number, b: number, c: number, d: number) { - this.startLineNumber = a - this.startColumn = b - this.endLineNumber = c - this.endColumn = d - } - } - const monaco = { Range: FakeRange } - - const diffEditor = { - getModel: () => ({ - original: { setValue: vi.fn(), getValue: () => '' }, - modified: { setValue: vi.fn(), getValue: () => state.diffModifiedValue }, - }), - getModifiedEditor: () => ({ revealLineInCenter: vi.fn() }), - } - - return { state, editor, monaco, diffEditor } -}) - -// --- Editor fakes ----------------------------------------------------------- - -vi.mock('@/components/interfaces/SQLEditor/MonacoEditor', async () => { - const { useEffect } = await vi.importActual('react') - return { - MonacoEditor: (props: any) => { - useEffect(() => { - props.editorRef.current = mocks.editor - props.monacoRef.current = mocks.monaco - props.onMount?.(mocks.editor) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - return ( -
    -
    {props.placeholder}
    - - -
    - ) - }, - } -}) - -vi.mock('@/components/ui/DiffEditor', async () => { - const { useEffect } = await vi.importActual('react') - return { - DiffEditor: (props: any) => { - useEffect(() => { - props.onMount?.(mocks.diffEditor) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - return
    - }, - } -}) - -vi.mock('@/components/ui/AIEditor/ResizableAIWidget', () => ({ - default: (props: any) => ( -
    - - - -
    - ), -})) - -// --- Child panel stubs (not under test) ------------------------------------- - -vi.mock('@/components/interfaces/SQLEditor/UtilityPanel/UtilityActions', () => ({ - UtilityActions: (props: any) => ( -
    - - - {String(props.isExecuting)} -
    - ), -})) - -vi.mock('@/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel', () => ({ - UtilityPanel: (props: any) => ( -
    - {props.activeTab} - -
    - ), -})) - -vi.mock('@/components/interfaces/SQLEditor/RunQueryWarningModal', () => ({ - RunQueryWarningModal: (props: any) => - props.visible ? ( -
    - - - -
    - ) : null, -})) - -// --- Context / selection hook stubs (orthogonal infra, not under test) ------ - -vi.mock('@/components/interfaces/SQLEditor/useAddDefinitions', () => ({ - useAddDefinitions: () => {}, -})) - -vi.mock('@/hooks/misc/useSelectedProject', () => ({ - useSelectedProjectQuery: () => ({ - data: { - id: 1, - ref: 'default', - connectionString: 'postgresql://postgres@localhost:5432/postgres', - }, - }), -})) - -vi.mock('@/hooks/misc/useSelectedOrganization', () => ({ - useSelectedOrganizationQuery: () => ({ data: { slug: 'test-org' } }), -})) - -vi.mock('@/hooks/misc/useOrgOptedIntoAi', () => ({ - useOrgAiOptInLevel: () => ({ - aiOptInLevel: 'disabled', - includeSchemaMetadata: false, - isHipaaProjectDisallowed: false, - }), -})) - -vi.mock('@/data/read-replicas/replicas-query', () => ({ - useReadReplicasQuery: () => ({ - data: [ - { - identifier: 'default', - connectionString: 'postgresql://postgres@localhost:5432/postgres', - }, - ], - isSuccess: true, - }), -})) - -vi.mock('@/data/database-event-triggers/database-event-triggers-query', () => ({ - useDatabaseEventTriggersQuery: () => ({ data: undefined }), -})) - -// `common` is globally mocked in vitestSetup to `{ ref: 'default' }`. Re-mock it -// here to also provide a snippet id (so `id` is stable and points at our seeded -// snippet) and a stable `useFlag`. -vi.mock('common', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useParams: () => ({ ref: 'default', id: SNIPPET_ID }), - useFlag: () => false, - } -}) - -// --- Helpers ---------------------------------------------------------------- - -function seedSnippet(sql: string, name = 'My query') { - ;(sqlEditorState.snippets as any)[SNIPPET_ID] = { - projectRef: 'default', - splitSizes: [50, 50], - snippet: { - id: SNIPPET_ID, - name, - project_id: 1, - owner_id: 1, - content: { sql, unchecked_sql: sql, schema_version: '1.0', favorite: false }, - }, - } -} - -function resetStores() { - for (const key of Object.keys(sqlEditorState.snippets)) { - delete (sqlEditorState.snippets as any)[key] - } - for (const key of Object.keys(sqlEditorSessionState.results)) { - delete (sqlEditorSessionState.results as any)[key] - } - sqlEditorDiffRequestState.pending = undefined -} - -const NON_EXPLAIN_ROWS = [{ id: 1, name: 'row-1' }] - -function mockQuerySuccess(rows: unknown[] = NON_EXPLAIN_ROWS) { - addAPIMock({ - method: 'post', - path: '/platform/pg-meta/:ref/query', - response: () => HttpResponse.json(rows), - }) -} - -function mockQueryError(body: Record) { - addAPIMock({ - method: 'post', - path: '/platform/pg-meta/:ref/query', - response: () => HttpResponse.json(body, { status: 400 }), - }) -} - -beforeEach(() => { - vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { - return setTimeout(() => cb(0), 0) as unknown as number - }) - resetStores() - seedSnippet('select 1;') - mocks.state.value = 'select 1;' - mocks.state.selection = null - mocks.state.selectionValue = undefined - mocks.state.decorations = ['decoration-1'] - mocks.editor.executeEdits.mockClear() - mocks.editor.deltaDecorations.mockClear() - mocks.editor.revealLineInCenter.mockClear() - mocks.editor.focus.mockClear() -}) - -afterEach(() => { - cleanup() - vi.unstubAllGlobals() - vi.restoreAllMocks() -}) - -async function renderEditor() { - const utils = customRender() - // The Monaco editor is loaded via next/dynamic; wait for the fake to mount. - await screen.findByTestId('monaco-editor') - return utils -} - -describe('SQLEditor characterization', () => { - test('1a. successful run adds result and keeps Results tab active', async () => { - const addResult = vi.spyOn(sqlEditorSessionState, 'addResult') - mockQuerySuccess(NON_EXPLAIN_ROWS) - - await renderEditor() - expect(screen.getByTestId('active-tab')).toHaveTextContent('results') - - fireEvent.click(screen.getByTestId('editor-run')) - - await waitFor(() => expect(addResult).toHaveBeenCalled()) - // The snippet id and rows are the load-bearing part; autoLimit is derived - // from the (SELECT) query + configured row limit and is asserted loosely. - expect(addResult.mock.calls[0][0]).toBe(SNIPPET_ID) - expect(addResult.mock.calls[0][1]).toEqual(NON_EXPLAIN_ROWS) - expect(screen.getByTestId('active-tab')).toHaveTextContent('results') - }) - - test('2. run error with position highlights the computed line and reveals it', async () => { - const addResultError = vi.spyOn(sqlEditorSessionState, 'addResultError') - mockQueryError({ - message: 'syntax error', - position: '8', - formattedError: 'ERROR: syntax error at or near "slect"\nLINE 3: slect 1;\n ^', - }) - - await renderEditor() - - fireEvent.click(screen.getByTestId('editor-run')) - - await waitFor(() => expect(mocks.editor.deltaDecorations).toHaveBeenCalled()) - - // With no selection, startLineNumber is 0 → highlighted line === parsed LINE (3). - const [oldDecorations, newDecorations] = mocks.editor.deltaDecorations.mock.calls[0] - expect(oldDecorations).toEqual([]) - expect(newDecorations[0].range.startLineNumber).toBe(3) - expect(newDecorations[0].range.endLineNumber).toBe(3) - expect(mocks.editor.revealLineInCenter).toHaveBeenCalledWith(3) - await waitFor(() => expect(addResultError).toHaveBeenCalled()) - }) - - test('2b. the next run clears the previously-set line highlights', async () => { - // First: produce a highlight. - mockQueryError({ - message: 'syntax error', - position: '8', - formattedError: 'ERROR: syntax error\nLINE 2: foo\n', - }) - await renderEditor() - fireEvent.click(screen.getByTestId('editor-run')) - await waitFor(() => expect(mocks.editor.deltaDecorations).toHaveBeenCalledTimes(1)) - - // Second run should clear the stored decorations before running again. - mockQuerySuccess(NON_EXPLAIN_ROWS) - fireEvent.click(screen.getByTestId('editor-run')) - await waitFor(() => - expect(mocks.editor.deltaDecorations).toHaveBeenCalledWith(['decoration-1'], []) - ) - }) - - test('3. running via the run button refocuses the editor', async () => { - mockQuerySuccess(NON_EXPLAIN_ROWS) - await renderEditor() - - fireEvent.click(screen.getByTestId('run-button')) - - await waitFor(() => expect(mocks.editor.focus).toHaveBeenCalled()) - }) - - test('3b. run button is disabled and short-circuits while a diff is open', async () => { - // Queue a diff request against a non-empty editor so a diff opens on mount. - mocks.state.value = 'select 1;' - sqlEditorDiffRequestState.requestDiff('select 42;', DiffType.Modification) - - await renderEditor() - // The queued request drains on mount and opens a diff, which disables Run. - // (Assert via the run button rather than the dynamically-imported diff editor.) - await waitFor(() => expect(screen.getByTestId('run-button')).toBeDisabled()) - - const addResult = vi.spyOn(sqlEditorSessionState, 'addResult') - // The run button is disabled while diffing; clicking should not execute. - fireEvent.click(screen.getByTestId('run-button')) - // Give any async work a chance to (not) happen. - await new Promise((r) => setTimeout(r, 20)) - expect(addResult).not.toHaveBeenCalled() - }) - - test('4. a diff request queued before mount drains exactly once', async () => { - // Empty editor → drain copies the SQL in via executeEdits('apply-ai-message'). - mocks.state.value = '' - sqlEditorDiffRequestState.requestDiff('select 100;', DiffType.Modification) - - const { unmount } = await renderEditor() - - await waitFor(() => - expect(mocks.editor.executeEdits).toHaveBeenCalledWith('apply-ai-message', expect.any(Array)) - ) - expect(mocks.editor.executeEdits).toHaveBeenCalledTimes(1) - // Request was consumed (drained), so it cannot re-apply. - expect(sqlEditorDiffRequestState.pending).toBeUndefined() - - // A fresh mount must NOT re-apply the already-consumed request. - unmount() - await renderEditor() - await new Promise((r) => setTimeout(r, 20)) - expect(mocks.editor.executeEdits).toHaveBeenCalledTimes(1) - }) - - test('5. the ask-ai widget renders only while the prompt is open', async () => { - mocks.state.value = 'select 1;' - await renderEditor() - - expect(screen.queryByTestId('ask-ai')).not.toBeInTheDocument() - - fireEvent.click(screen.getByTestId('editor-prompt')) - expect(await screen.findByTestId('ask-ai')).toBeInTheDocument() - - fireEvent.click(screen.getByTestId('ask-ai-cancel')) - await waitFor(() => expect(screen.queryByTestId('ask-ai')).not.toBeInTheDocument()) - }) - - test('6. a destructive query opens the warning modal; confirm re-runs forced', async () => { - mocks.state.value = 'drop table foo;' - let forcedQuery = '' - addAPIMock({ - method: 'post', - path: '/platform/pg-meta/:ref/query', - response: async ({ request }) => { - const body = (await request.json()) as { query: string } - forcedQuery = body.query - return HttpResponse.json(NON_EXPLAIN_ROWS) - }, - }) - - await renderEditor() - - fireEvent.click(screen.getByTestId('editor-run')) - - // Destructive query → confirmation modal, and no query is sent yet. - expect(await screen.findByTestId('warning-modal')).toBeInTheDocument() - expect(forcedQuery).toBe('') - - // Confirming forces the (same) destructive query to actually run. - fireEvent.click(screen.getByTestId('warn-confirm')) - await waitFor(() => expect(forcedQuery).toMatch(/drop table foo/i)) - }) - - test('6b. confirm-with-RLS re-runs with appended enable-RLS statements', async () => { - mocks.state.value = 'create table foo (id int);' - let capturedQuery = '' - addAPIMock({ - method: 'post', - path: '/platform/pg-meta/:ref/query', - response: async ({ request }) => { - const body = (await request.json()) as { query: string } - capturedQuery = body.query - return HttpResponse.json(NON_EXPLAIN_ROWS) - }, - }) - - await renderEditor() - - fireEvent.click(screen.getByTestId('editor-run')) - expect(await screen.findByTestId('warning-modal')).toBeInTheDocument() - - fireEvent.click(screen.getByTestId('warn-confirm-rls')) - await waitFor(() => expect(capturedQuery).toMatch(/enable row level security/i)) - }) -}) diff --git a/e2e/studio/features/sql-editor.spec.ts b/e2e/studio/features/sql-editor.spec.ts index 1c7b02f5e0a..a2d279d1a4e 100644 --- a/e2e/studio/features/sql-editor.spec.ts +++ b/e2e/studio/features/sql-editor.spec.ts @@ -406,6 +406,84 @@ test.describe('SQL Editor', () => { } }) + test('destructive query warning modal: confirm re-runs the forced query', async ({ ref }) => { + await expect(page.getByText('Loading...')).not.toBeVisible() + await page.locator('.view-lines').click() + await page.keyboard.press('ControlOrMeta+KeyA') + await page.keyboard.type(`drop table pw_sql_editor_confirm_dne_e2e;`) + + // Track whether the SQL editor dispatches this specific query to pg-meta + let queryDispatched = false + const listener = (request: any) => { + if ( + request.url().includes('query?key=') && + request.method() === 'POST' && + request.postData()?.includes('drop table pw_sql_editor_confirm_dne_e2e') + ) { + queryDispatched = true + } + } + page.on('request', listener) + + await page.getByTestId('sql-run-button').click() + + // Destructive query -> confirmation modal, and no query is sent yet. + await expect(page.getByRole('heading', { name: 'Potential issue detected' })).toBeVisible() + expect(queryDispatched).toBe(false) + + // Confirming forces the (same) destructive query to actually run. + const sqlMutationPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) + await page.getByRole('button', { name: 'Run query' }).click() + await sqlMutationPromise + expect(queryDispatched).toBe(true) + + page.removeListener('request', listener) + + // clear SQL snippet + if (!isCLI()) { + await deleteSqlSnippet(page, ref, newSqlSnippetName) + } else { + await page.reload() + } + }) + + test('debug button opens the AI Assistant with the query error pre-filled', async ({ ref }) => { + await expect(page.getByText('Loading...')).not.toBeVisible() + await page.locator('.view-lines').click() + await page.keyboard.press('ControlOrMeta+KeyA') + await page.keyboard.type(`select * from pw_sql_editor_missing_table_e2e;`) + + const sqlMutationPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) + await page.getByTestId('sql-run-button').click() + await sqlMutationPromise + + const debugButton = page.getByRole('button', { name: 'Debug with Assistant' }) + await expect(debugButton, 'Debug button should appear once the query errors').toBeVisible() + await debugButton.click() + + await expect( + page.getByRole('button', { name: 'Close Assistant' }), + 'AI Assistant panel should open' + ).toBeVisible() + await expect( + page.getByRole('button', { name: 'Debug SQL snippet' }), + 'Assistant chat should be pre-named for the debug flow' + ).toBeVisible() + + await page.getByRole('button', { name: 'Close Assistant' }).click() + + // clear SQL snippet + if (!isCLI()) { + await deleteSqlSnippet(page, ref, newSqlSnippetName) + } else { + await page.reload() + } + }) + test('should not show warning modal for safe alter database statement', async ({ ref }) => { await expect(page.getByText('Loading...')).not.toBeVisible() await page.locator('.view-lines').click() From 3bca21b3f8b1b19c8b3d2b1fa49fa24cffa6541b Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:52:22 -0400 Subject: [PATCH 004/141] chore(a11y): convert leftover focus recipes to focus-ring (#48219) ## What kind of change does this PR introduce? Accessibility cleanup (DEPR-628). ## What is the current behavior? Leftover call sites still use ad-hoc focus recipes (`ring-foreground-muted`, `outline-brand`, Dialog/Sheet `focus:` rings, etc.) instead of the shared utilities from #41575. ## What is the new behavior? Converts those leftovers across `packages/ui`, Studio, www, docs, and design-system to `focus-ring`, preferring `focus-visible`. Keeps documented exceptions (`group-focus-visible`, InputGroup `:has()`). ## To test Tab through controls (keyboard only). Expect a consistent offset ring on `:focus-visible`, not a green/brand/custom stack, and no ring animation. ### www (marketing) Preview: https://zone-www-dot-com-git-danny-depr-628-focus-ring-fbccf9-supabase.vercel.app - Global nav on `/`: Product, Developers, Solutions dropdowns; logo; hamburger + mobile menu - `/features`: view toggles and feature cards - `/company`: card links - `/changelog`: timeline / entry links - `/partners/catalog`: grid/list toggle and partner cards - `/pricing`: compute section expand control - Product / Modules / Solutions sticky navs on product pages (e.g. `/database`, `/storage`) - `/state-of-startups`: TwoOptionToggle if present ### docs Preview: https://docs-git-danny-depr-628-focus-ring-long-tail-supabase.vercel.app - Any guide page: top nav dropdowns and items - Narrow viewport: hamburger, then mobile menu links + close - Guide with PromptPanel / tabs: tab to prompt actions and tab list ### studio (dashboard) Preview: https://studio-staging-git-danny-depr-628-focus-ring-long-tail-supabase.vercel.app - Project home: Connect section tiles; drag-handle focus on sortable sections - Integrations marketplace (`/project//integrations`): featured cards, list/grid toggle, list rows - Auth (`/project//auth/oauth-apps`, `/project//auth/providers`): open create/edit sheet, tab to close (X) - Database policies (`/project//database/policies`): open policy editor sheet, tab to close - Storage policies (`/project//storage/files/policies`): bucket section links; policy modal close - Query performance (`/project//observability/query-performance`): info icon buttons on metrics - Replication pipeline detail (if available): slot lag / status info icons - Support (`/support/new`): attachment add/remove controls - Table editor: spreadsheet import preview checkboxes; row text/JSON editor TwoOptionToggle - Any Dialog/Sheet/toast close (X): ring on keyboard focus only, not mouse click ### design-system Preview: https://design-system-git-danny-depr-628-focus-ring-long-tail-supabase.vercel.app - Colour palette swatches (keyboard focus) - Form patterns sidepanel example: avatar / focusable control in the example ## Additional context - Linear: [DEPR-628](https://linear.app/supabase/issue/DEPR-628) - Follow-ups: form-group CSS (DEPR-629), Storage columns selection (DEPR-630), ESLint rule (DEPR-632) ## Summary by CodeRabbit * **Accessibility & Usability** * Standardized keyboard focus indicators across navigation, dialogs, forms, buttons, toggles, links, and tooltips using a consolidated focus style. * Improved toggle controls to use proper button semantics (instead of clickable text), including `aria-pressed`/disabled handling and better keyboard navigation. * **Visual Updates** * Harmonized hover/focus ring visuals across the design system, Studio, documentation, and marketing pages while preserving existing layout and interaction behavior. --- .../components/color-palette.tsx | 2 +- .../example/form-patterns-sidepanel.tsx | 2 +- .../NavigationMenu/GlobalMobileMenu.tsx | 4 +- .../NavigationMenu/GlobalNavigationMenu.tsx | 4 +- apps/docs/features/ui/PromptPanel.tsx | 4 +- apps/docs/features/ui/Tabs.tsx | 2 +- .../CreateOrUpdateCustomProviderSheet.tsx | 6 +-- .../OAuthApps/CreateOrUpdateOAuthAppSheet.tsx | 6 +-- .../PolicyEditorPanelHeader.tsx | 5 +- .../SlotLagMetrics.tsx | 2 +- .../ReplicationPipelineStatus/SlotStatus.tsx | 2 +- .../Marketplace/MarketplaceCard.tsx | 2 +- .../MarketplaceFeaturedHeroGrid.tsx | 4 +- .../Marketplace/MarketplaceFilterBar.tsx | 4 +- .../Marketplace/MarketplaceListRow.tsx | 7 +-- .../interfaces/ProjectHome/ConnectSection.tsx | 2 +- .../ProjectHome/SortableSection.tsx | 3 +- .../QueryPerformanceMetrics.tsx | 4 +- .../StoragePoliciesBucketsSection.tsx | 2 +- .../StoragePolicyEditorModalTitle.tsx | 4 +- .../interfaces/Support/AttachmentUpload.tsx | 6 +-- .../Support/SupportAssistantSuccessCard.tsx | 2 +- .../SpreadsheetImportPreview.tsx | 2 +- .../SupportAssistantSuccessCardContent.tsx | 2 +- apps/studio/components/ui/TwoOptionToggle.tsx | 20 +++++--- .../partners/catalog/IntegrationsContent.tsx | 6 +-- .../components/TwoOptionToggle.tsx | 46 +++++++++++-------- apps/www/components/Announcement/Badge.tsx | 2 +- apps/www/components/Button.tsx | 4 +- .../Changelog/ChangelogTimelineList.tsx | 4 +- apps/www/components/FeaturesMatrix.tsx | 2 +- apps/www/components/Modules/ModulesNav.tsx | 4 +- .../www/components/Nav/DevelopersDropdown.tsx | 6 +-- apps/www/components/Nav/HamburgerMenu.tsx | 2 +- apps/www/components/Nav/MenuItem.tsx | 2 +- apps/www/components/Nav/MobileMenu.tsx | 12 ++--- apps/www/components/Nav/ProductDropdown.tsx | 12 ++--- .../components/Nav/RightClickBrandLogo.tsx | 2 +- apps/www/components/Nav/SolutionsDropdown.tsx | 7 ++- apps/www/components/Nav/index.tsx | 2 +- .../Pricing/PricingComputeSection.tsx | 19 ++++---- apps/www/components/Products/ProductCard.tsx | 2 +- apps/www/components/Products/ProductsNav.tsx | 2 +- apps/www/components/SolutionsStickyNav.tsx | 2 +- apps/www/pages/changelog.tsx | 2 +- apps/www/pages/company.tsx | 4 +- apps/www/pages/features.tsx | 6 +-- packages/ui-patterns/src/TextLink/index.tsx | 2 +- .../src/multi-select/multi-select.tsx | 4 +- .../ui/src/components/radio-group-stacked.tsx | 13 +++--- .../ui/src/components/shadcn/ui/dialog.tsx | 4 +- .../ui/src/components/shadcn/ui/sheet.tsx | 2 +- .../ui/src/components/shadcn/ui/sonner.tsx | 6 +-- 53 files changed, 142 insertions(+), 142 deletions(-) diff --git a/apps/design-system/components/color-palette.tsx b/apps/design-system/components/color-palette.tsx index 92f85bdbd8c..f568da8c511 100644 --- a/apps/design-system/components/color-palette.tsx +++ b/apps/design-system/components/color-palette.tsx @@ -64,7 +64,7 @@ const ColorPalette = () => { key={step * 100} type="button" onClick={() => handleCopy(reference)} - className="group relative flex aspect-square w-full items-center justify-center rounded-sm border border-overlay/40 transition hover:scale-[1.05] focus:outline-none focus-visible:ring-2 focus-visible:ring-foreground" + className="group relative flex aspect-square w-full items-center justify-center rounded-sm border border-overlay/40 transition-transform hover:scale-[1.05] focus-ring" style={{ backgroundColor: reference }} title={reference} > diff --git a/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx index 0ca56bf42ac..1b3a146e357 100644 --- a/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx +++ b/apps/design-system/registry/default/example/form-patterns-sidepanel.tsx @@ -319,7 +319,7 @@ export default function FormPatternsSidePanel() { tabIndex={0} type="button" onClick={() => uploadButtonRef.current?.click()} - className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-visible:outline-brand-600" + className="flex items-center justify-center h-10 w-10 shrink-0 text-foreground-lighter hover:text-foreground-light overflow-hidden rounded-full bg-cover border hover:border-strong focus-ring" style={{ backgroundImage: logoUrl ? `url("${logoUrl}")` : 'none', }} diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx index b3f69894d6c..619222551c2 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx @@ -29,7 +29,7 @@ const listItem = { } const itemClassName = - 'block py-2 pl-2 pr-3.5 text-sm text-foreground-light hover:bg-surface-200 focus-visible:ring-2 focus-visible:outline-hidden focus-visible:ring-foreground-lighter focus-visible:rounded-sm' + 'block py-2 pl-2 pr-3.5 text-sm text-foreground-light hover:bg-surface-200 focus-ring rounded-sm' const AccordionMenuItem = ({ section }: { section: DropdownMenuItem[] }) => { const activeLabel = useActiveMenuLabel(GLOBAL_MENU_ITEMS) @@ -145,7 +145,7 @@ const GlobalMobileMenu = ({ open, setOpen }: Props) => { tabIndex={0} onClick={() => setOpen(false)} type="button" - className="inline-flex items-center justify-center focus:ring-brand bg-surface-100 hover:bg-surface-200 focus:outline-hidden focus:ring-2 focus:ring-inset border border-default bg-surface-100/75 text-foreground-light rounded-sm min-w-[30px] w-[30px] h-[30px]" + className="inline-flex items-center justify-center bg-surface-100 hover:bg-surface-200 border border-default bg-surface-100/75 text-foreground-light rounded-sm min-w-[30px] w-[30px] h-[30px] focus-ring" > Close menu diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx index 3b6c71ed599..bd3c81b8211 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx @@ -59,7 +59,7 @@ export const useActiveMenuLabel = (menu: typeof GLOBAL_MENU_ITEMS) => { const GlobalNavigationMenu: FC = () => { const activeLabel = useActiveMenuLabel(GLOBAL_MENU_ITEMS) const triggerClassName = - 'h-(--header-height) p-2 bg-transparent border-0 border-b-2 border-transparent font-normal rounded-none text-foreground-light hover:bg-transparent hover:text-foreground data-open:bg-transparent! data-open:text-foreground! data-radix-collection-item:focus-visible:ring-2 data-radix-collection-item:focus-visible:ring-foreground-lighter data-radix-collection-item:focus-visible:text-foreground h-full focus-visible:rounded-sm shadow-none! outline-hidden transition-all outline-0 focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-brand-600' + 'h-(--header-height) p-2 bg-transparent border-0 border-b-2 border-transparent font-normal rounded-none text-foreground-light hover:bg-transparent hover:text-foreground data-open:bg-transparent! data-open:text-foreground! focus-ring focus-visible:text-foreground h-full focus-visible:rounded-sm shadow-none!' return (
    @@ -172,7 +172,7 @@ export const MenuItem = React.forwardRef< ref={ref} className={cn( 'group/menu-item flex items-center gap-2', - 'w-full flex h-8 items-center text-foreground-light text-sm hover:text-foreground select-none rounded-md p-2 leading-none no-underline outline-hidden! focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:text-foreground', + 'w-full flex h-8 items-center text-foreground-light text-sm hover:text-foreground select-none rounded-md p-2 leading-none no-underline focus-ring focus-visible:text-foreground', className )} {...props} diff --git a/apps/docs/features/ui/PromptPanel.tsx b/apps/docs/features/ui/PromptPanel.tsx index 9182f290b42..8155548b703 100644 --- a/apps/docs/features/ui/PromptPanel.tsx +++ b/apps/docs/features/ui/PromptPanel.tsx @@ -133,7 +133,7 @@ function ExpandableContent({ children }: { children: ReactNode }) { tabIndex={0} type="button" onClick={() => setIsExpanded((expanded) => !expanded)} - className="mt-2 text-sm text-brand-link transition-colors hover:text-brand focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted" + className="mt-2 text-sm text-brand-link transition-colors hover:text-brand focus-ring" aria-expanded={isExpanded} > {isExpanded ? 'Show less' : 'Show more'} @@ -192,7 +192,7 @@ function CopyButton({ label, value }: { label: string; value: string }) { setCopied(true) }) }} - className="rounded-sm p-1.5 text-foreground-muted transition-colors hover:bg-surface-200 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted" + className="rounded-sm p-1.5 text-foreground-muted transition-colors hover:bg-surface-200 hover:text-foreground focus-ring" aria-label={copied ? `${label} copied` : `Copy ${label}`} title={copied ? 'Copied' : 'Copy to clipboard'} > diff --git a/apps/docs/features/ui/Tabs.tsx b/apps/docs/features/ui/Tabs.tsx index c227a870620..2c01f57be29 100644 --- a/apps/docs/features/ui/Tabs.tsx +++ b/apps/docs/features/ui/Tabs.tsx @@ -55,7 +55,7 @@ export const tabsListVariants = cva(cn('flex'), { export const tabsTriggerListVariants = cva( cn( - 'relative cursor-pointer flex items-center space-x-2 text-center transition focus:outline-hidden focus-visible:ring-3 focus-visible:ring-foreground-muted focus-visible:border-foreground-muted' + 'relative cursor-pointer flex items-center space-x-2 text-center transition-colors focus-ring' ), { variants: { diff --git a/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx b/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx index 1fa18fbba32..c46a10f81e4 100644 --- a/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx +++ b/apps/studio/components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx @@ -260,10 +260,8 @@ export const CreateOrUpdateCustomProviderSheet = ({
    diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx index ff6e87750ca..a44a80a8d7c 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx @@ -263,10 +263,8 @@ export const CreateOrUpdateOAuthAppSheet = ({
    diff --git a/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyEditorPanelHeader.tsx b/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyEditorPanelHeader.tsx index 60b646d64c2..debf11e9d9d 100644 --- a/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyEditorPanelHeader.tsx +++ b/apps/studio/components/interfaces/Database/Policies/PolicyEditorPanel/PolicyEditorPanelHeader.tsx @@ -30,9 +30,8 @@ export const PolicyEditorPanelHeader = ({
    diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx index 9c69b77f5ad..767322c381a 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotLagMetrics.tsx @@ -135,7 +135,7 @@ export const SlotLagMetricsList = ({ type="button" tabIndex={0} aria-label={`What is ${field.label}`} - className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter" + className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-ring" > diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotStatus.tsx index 85bd503a804..3bbeada6407 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/SlotStatus.tsx @@ -66,7 +66,7 @@ export const SlotStatusLegend = () => { type="button" tabIndex={0} aria-label="What do the slot statuses mean?" - className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter" + className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-ring" > diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceCard.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceCard.tsx index 0b1d6b3eb37..a32b1731b24 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceCard.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceCard.tsx @@ -18,7 +18,7 @@ export const MarketplaceCard = ({ integration, isInstalled }: MarketplaceCardPro return (
    diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx index 05d5dcaf01a..8a0fe9880fe 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx @@ -113,7 +113,7 @@ export const MarketplaceFeaturedHeroGrid = ({
    @@ -170,7 +170,7 @@ export const MarketplaceFeaturedHeroGrid = ({
    diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFilterBar.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFilterBar.tsx index b6678268947..0ea9161959f 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFilterBar.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFilterBar.tsx @@ -220,7 +220,7 @@ export const MarketplaceFilterBar = ({ onClick={() => onViewModeChange('grid')} className={cn( 'border-r px-2 py-1.5 rounded-l-md cursor-pointer', - 'focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background', + 'focus-visible:z-10 focus-ring', viewMode === 'grid' ? 'bg-surface-200 text-foreground' : 'text-foreground-light hover:bg-surface-100' @@ -235,7 +235,7 @@ export const MarketplaceFilterBar = ({ onClick={() => onViewModeChange('list')} className={cn( 'px-2 py-1.5 rounded-r-md cursor-pointer', - 'focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background', + 'focus-visible:z-10 focus-ring', viewMode === 'list' ? 'bg-surface-200 text-foreground' : 'text-foreground-light hover:bg-surface-100' diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceListRow.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceListRow.tsx index dc5055e059b..b19413b0511 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceListRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceListRow.tsx @@ -33,12 +33,9 @@ export const MarketplaceListRow = ({ integration, isInstalled }: MarketplaceList - +
    -
    +
    {integration.name}
    diff --git a/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx b/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx index 587ba378057..1bf15867cc5 100644 --- a/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx +++ b/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx @@ -92,7 +92,7 @@ export const ConnectSection = () => { onClick={() => handleActionClick(action)} className={cn( 'group flex items-center gap-3 p-4 text-left transition-colors min-h-[72px] w-full', - 'hover:bg-surface-100 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand', + 'hover:bg-surface-100 focus-ring', 'xl:min-h-32 xl:flex-col xl:justify-center xl:p-6 xl:text-center', ((action.requiresActiveProject ?? true) ? !isActiveHealthy diff --git a/apps/studio/components/interfaces/ProjectHome/SortableSection.tsx b/apps/studio/components/interfaces/ProjectHome/SortableSection.tsx index 3074852d286..9502472e12f 100644 --- a/apps/studio/components/interfaces/ProjectHome/SortableSection.tsx +++ b/apps/studio/components/interfaces/ProjectHome/SortableSection.tsx @@ -27,8 +27,7 @@ export const SortableSection = ({ id, children }: SortableSectionProps) => { aria-label="Drag to reorder section" className={cn( 'absolute -left-6 top-2 text-foreground-muted hover:text-foreground cursor-grab active:cursor-grabbing', - 'rounded-sm outline-hidden', - 'focus-visible:outline-solid focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-border-strong' + 'rounded-sm focus-ring' )} {...attributes} {...listeners} diff --git a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceMetrics.tsx b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceMetrics.tsx index 7063136cca5..cf6966ee7c2 100644 --- a/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceMetrics.tsx +++ b/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceMetrics.tsx @@ -71,7 +71,7 @@ export const QueryPerformanceMetrics = () => { type="button" tabIndex={0} aria-label="How are slow queries calculated?" - className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter" + className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-ring" onClick={(e) => { e.stopPropagation() }} @@ -92,7 +92,7 @@ export const QueryPerformanceMetrics = () => { type="button" tabIndex={0} aria-label={`What is ${card.title}?`} - className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter" + className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-ring" onClick={(e) => { e.stopPropagation() }} diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketsSection.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketsSection.tsx index 98cff2d6550..42835b9e1cc 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketsSection.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePoliciesBucketsSection.tsx @@ -72,7 +72,7 @@ export const BucketsPolicies = ({ aria-label="Toggle bucket list" className={cn( 'rounded-md p-1 text-foreground-light hover:text-foreground', - 'outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background' + 'focus-ring' )} > diff --git a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx index 8f837ab6c38..137403b1580 100644 --- a/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx +++ b/apps/studio/components/interfaces/Storage/StoragePolicies/StoragePolicyEditorModalTitle.tsx @@ -32,7 +32,7 @@ export const StoragePolicyEditorModalTitle = ({ tabIndex={0} onClick={onSelectBackFromTemplates} className={cn( - 'cursor-pointer rounded-xs opacity-20 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-foreground-muted', + 'cursor-pointer rounded-xs opacity-20 hover:opacity-100 focus-ring disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-foreground-muted', 'hit-area-6' )} > @@ -51,7 +51,7 @@ export const StoragePolicyEditorModalTitle = ({ diff --git a/apps/studio/components/interfaces/Support/AttachmentUpload.tsx b/apps/studio/components/interfaces/Support/AttachmentUpload.tsx index 18613095630..bcb7791fddb 100644 --- a/apps/studio/components/interfaces/Support/AttachmentUpload.tsx +++ b/apps/studio/components/interfaces/Support/AttachmentUpload.tsx @@ -24,7 +24,7 @@ const MAX_ATTACHMENTS = 5 const removeAttachmentButtonClassName = cn( 'absolute -top-1 -right-1 size-4 shrink-0 rounded-full bg-red-900 p-0 cursor-pointer', - 'outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background' + 'focus-ring' ) const RemoveAttachmentIcon = () => ( @@ -257,9 +257,9 @@ export function AttachmentUploadDisplay({ tabIndex={0} aria-label="Add attachment" className={cn( - 'border border-stronger opacity-50 transition hover:opacity-100', + 'border border-stronger opacity-50 hover:opacity-100', 'group flex h-14 w-14 cursor-pointer items-center justify-center rounded-sm', - 'outline-hidden focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background' + 'focus-visible:opacity-100 focus-ring' )} onClick={addFile} > diff --git a/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.tsx b/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.tsx index 2c88dd111d1..064b55e557f 100644 --- a/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.tsx +++ b/apps/studio/components/interfaces/Support/SupportAssistantSuccessCard.tsx @@ -46,7 +46,7 @@ function SupportAssistantSuccessCardLoadingShell({ className }: { className?: st tabIndex={0} aria-label="Open assistant response" className={cn( - 'group cursor-pointer bg-muted/50 transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand', + 'group cursor-pointer bg-muted/50 transition-colors hover:bg-muted/50 focus-ring', className )} > diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx index 67268cdfeee..2b1ae0abcfd 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImportPreview.tsx @@ -179,7 +179,7 @@ export const SpreadsheetImportPreview = ({ ) if (!isDisabled || !disabledOptionTooltip) return optionButton diff --git a/apps/www/app/partners/catalog/IntegrationsContent.tsx b/apps/www/app/partners/catalog/IntegrationsContent.tsx index 00ff3172e70..c1651c96c85 100644 --- a/apps/www/app/partners/catalog/IntegrationsContent.tsx +++ b/apps/www/app/partners/catalog/IntegrationsContent.tsx @@ -314,7 +314,7 @@ export default function IntegrationsContent({ title="Grid view" onClick={() => setFilters({ view: 'grid' })} className={cn( - 'relative flex items-center justify-center w-8 h-8 transition-colors rounded-l-lg focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:ring-offset-1 focus-visible:ring-offset-background', + 'relative flex items-center justify-center w-8 h-8 rounded-l-lg focus-visible:z-10 focus-ring', viewMode === 'grid' ? 'bg-surface-300 text-foreground' : 'bg-surface-75 text-foreground-muted hover:text-foreground hover:bg-surface-200' @@ -327,7 +327,7 @@ export default function IntegrationsContent({ title="List view" onClick={() => setFilters({ view: 'list' })} className={cn( - 'relative flex items-center justify-center w-8 h-8 transition-colors border-l border-muted rounded-r-lg focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:ring-offset-1 focus-visible:ring-offset-background', + 'relative flex items-center justify-center w-8 h-8 border-l border-muted rounded-r-lg focus-visible:z-10 focus-ring', viewMode === 'list' ? 'bg-surface-300 text-foreground' : 'bg-surface-75 text-foreground-muted hover:text-foreground hover:bg-surface-200' @@ -346,7 +346,7 @@ export default function IntegrationsContent({ `absolute top-0 z-1 text-xs inline-flex h-full items-center justify-center font-medium ${ isActive ? 'hover:text-foreground-light hover:text-foreground' : 'hover:text-foreground' - } hover:text-foreground focus:z-10 focus:outline-hidden focus:border-blue-300 focus:ring-blue - transition ease-in-out duration-150` + } hover:text-foreground focus-visible:z-10 focus-ring` return (
    - {options.map((option: any, index: number) => ( - { + const isActive = activeOption === option + return ( + + ) + })}
    ) } diff --git a/apps/www/components/Announcement/Badge.tsx b/apps/www/components/Announcement/Badge.tsx index 0009742b795..500ad764ff8 100644 --- a/apps/www/components/Announcement/Badge.tsx +++ b/apps/www/components/Announcement/Badge.tsx @@ -48,7 +48,7 @@ const AnnouncementBadge = ({ hover:border-foreground-muted/30 shadow-md overflow-hidden - focus-visible:outline-hidden focus-visible:ring-brand-600 focus-visible:ring-2 focus-visible:rounded-full + focus-ring rounded-full `, !badge && 'pl-5' )} diff --git a/apps/www/components/Button.tsx b/apps/www/components/Button.tsx index 93bf7f63a5e..88b9c32f605 100644 --- a/apps/www/components/Button.tsx +++ b/apps/www/components/Button.tsx @@ -11,7 +11,7 @@ const Button = (props: Props) => { const colorClass = type === 'primary' - ? 'px-3 py-2 shadow-xs border border-transparent text-white bg-brand-400 hover:bg-brand-300 focus:ring-2 focus:ring-offset-2 focus:ring-brand-300' + ? 'px-3 py-2 shadow-xs border border-transparent text-white bg-brand-400 hover:bg-brand-300' : 'text-brand-400 bg-none' const textClass = type === 'primary' ? 'font-medium left-3 group-hover:left-0' : 'font-normal' @@ -27,7 +27,7 @@ const Button = (props: Props) => { const sharedClassName = ` group inline-flex items-center rounded-md text-sm - leading-4 transition focus:outline-hidden ${colorClass} ${className} + leading-4 focus-ring ${colorClass} ${className} ` const content = ( diff --git a/apps/www/components/Changelog/ChangelogTimelineList.tsx b/apps/www/components/Changelog/ChangelogTimelineList.tsx index a75c04c3e4f..acd81b4a600 100644 --- a/apps/www/components/Changelog/ChangelogTimelineList.tsx +++ b/apps/www/components/Changelog/ChangelogTimelineList.tsx @@ -38,8 +38,8 @@ export function LabelBadges({ href={changelogTagFilterUrl(label.name)} className={ tiny - ? 'inline-flex shrink-0 no-underline focus-visible:ring-brand-default rounded-sm focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden' - : 'inline-flex shrink-0 no-underline focus-visible:ring-brand-default rounded-md focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden' + ? 'inline-flex shrink-0 no-underline focus-ring rounded-sm' + : 'inline-flex shrink-0 no-underline focus-ring rounded-md' } onClick={onBadgeClick} > diff --git a/apps/www/components/FeaturesMatrix.tsx b/apps/www/components/FeaturesMatrix.tsx index 420fae4dfe7..d62adbff94d 100644 --- a/apps/www/components/FeaturesMatrix.tsx +++ b/apps/www/components/FeaturesMatrix.tsx @@ -170,7 +170,7 @@ export function FeaturesMatrix({ features }: FeaturesMatrixProps) {
    diff --git a/apps/www/components/Modules/ModulesNav.tsx b/apps/www/components/Modules/ModulesNav.tsx index ffc70394298..d1451353121 100644 --- a/apps/www/components/Modules/ModulesNav.tsx +++ b/apps/www/components/Modules/ModulesNav.tsx @@ -23,7 +23,7 @@ function ModulesNav({ activePage, docsUrl }: Props) { key={currentModule.name} className={cn( 'flex items-center gap-1.5 px-2 first:-ml-2 py-3 border-b border-transparent text-sm text-foreground-lighter hover:text-foreground', - 'focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:text-foreground focus-visible:outline-brand-600', + 'focus-ring focus-visible:text-foreground', currentModule.name === activePage && 'border-foreground-light text-foreground' )} href={currentModule.url ?? ''} @@ -52,7 +52,7 @@ function ModulesNav({ activePage, docsUrl }: Props) { diff --git a/apps/www/components/Nav/DevelopersDropdown.tsx b/apps/www/components/Nav/DevelopersDropdown.tsx index 8c9de8c27cb..58fc8b4e087 100644 --- a/apps/www/components/Nav/DevelopersDropdown.tsx +++ b/apps/www/components/Nav/DevelopersDropdown.tsx @@ -27,7 +27,7 @@ export const DevelopersDropdown = () => {
  • {Icon && } {link.text} @@ -51,7 +51,7 @@ export const DevelopersDropdown = () => {
    Blog @@ -61,7 +61,7 @@ export const DevelopersDropdown = () => {
  • {post.title} diff --git a/apps/www/components/Nav/HamburgerMenu.tsx b/apps/www/components/Nav/HamburgerMenu.tsx index 6d141c145a7..61386798fc7 100644 --- a/apps/www/components/Nav/HamburgerMenu.tsx +++ b/apps/www/components/Nav/HamburgerMenu.tsx @@ -11,7 +11,7 @@ const HamburgerButton = (props: HamburgerButtonProps) => (

  • diff --git a/apps/www/components/Products/ProductCard.tsx b/apps/www/components/Products/ProductCard.tsx index 25e5d86960e..6187050f45f 100644 --- a/apps/www/components/Products/ProductCard.tsx +++ b/apps/www/components/Products/ProductCard.tsx @@ -30,7 +30,7 @@ const ProductCard = ({ {changelogLabelDisplayName(label.name)} diff --git a/apps/www/pages/company.tsx b/apps/www/pages/company.tsx index ef209842ff3..6574e23327e 100644 --- a/apps/www/pages/company.tsx +++ b/apps/www/pages/company.tsx @@ -220,7 +220,7 @@ const Press = () => { href={x.href} key={x.href} target="_blank" - className="flex flex-col justify-start items-stretch group cursor-pointer transition rounded-xl focus-visible:ring-2 focus-visible:ring-foreground-lighter outline-hidden outline-0 focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-foreground-lighter" + className="flex flex-col justify-start items-stretch group cursor-pointer rounded-xl focus-ring" > { href={x.href} key={x.href} target="_blank" - className="flex flex-col justify-start items-stretch group cursor-pointer transition rounded-xl focus-visible:ring-2 focus-visible:ring-foreground-lighter outline-hidden outline-0 focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-foreground-lighter" + className="flex flex-col justify-start items-stretch group cursor-pointer rounded-xl focus-ring" > setViewMode('grid')} className={cn( - 'relative flex items-center justify-center w-8 h-8 transition-colors rounded-l-lg focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:ring-offset-1 focus-visible:ring-offset-background', + 'relative flex items-center justify-center w-8 h-8 rounded-l-lg focus-visible:z-10 focus-ring', viewMode === 'grid' ? 'bg-surface-300 text-foreground' : 'bg-surface-75 text-foreground-muted hover:text-foreground hover:bg-surface-200' @@ -293,7 +293,7 @@ function FeaturesPage() { title="Matrix view" onClick={() => setViewMode('matrix')} className={cn( - 'relative flex items-center justify-center w-8 h-8 transition-colors border-l border-muted rounded-r-lg focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:ring-offset-1 focus-visible:ring-offset-background', + 'relative flex items-center justify-center w-8 h-8 border-l border-muted rounded-r-lg focus-visible:z-10 focus-ring', viewMode === 'matrix' ? 'bg-surface-300 text-foreground' : 'bg-surface-75 text-foreground-muted hover:text-foreground hover:bg-surface-200' @@ -316,7 +316,7 @@ function FeaturesPage() { @@ -66,12 +66,11 @@ const RadioGroupStackedItem = React.forwardRef< // Base styles 'aspect-square h-4 w-4 min-w-4 min-h-4 rounded-full border relative', 'flex items-center justify-center', - 'ring-offset-background transition', // States 'group-data-[state=checked]:border-foreground-muted', - 'group-focus:border-foreground-muted group-focus:outline-hidden', - 'group-focus-visible:ring-2 group-focus-visible:ring-background-control group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-foreground-muted', - 'group-hover:border-foreground-muted' + 'group-focus-visible:border-foreground-muted', + 'group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-background', + 'group-hover:border-foreground-muted transition-colors' )} > diff --git a/packages/ui/src/components/shadcn/ui/dialog.tsx b/packages/ui/src/components/shadcn/ui/dialog.tsx index 5edd2b3318e..226ef4b0003 100644 --- a/packages/ui/src/components/shadcn/ui/dialog.tsx +++ b/packages/ui/src/components/shadcn/ui/dialog.tsx @@ -114,7 +114,7 @@ const DialogContent = React.forwardRef< {!hideClose && ( @@ -197,7 +197,7 @@ const DialogClose = React.forwardRef< diff --git a/packages/ui/src/components/shadcn/ui/sonner.tsx b/packages/ui/src/components/shadcn/ui/sonner.tsx index fcdee422e32..956c760a22f 100644 --- a/packages/ui/src/components/shadcn/ui/sonner.tsx +++ b/packages/ui/src/components/shadcn/ui/sonner.tsx @@ -54,10 +54,10 @@ const SonnerToaster = ({ toastOptions, ...props }: ToasterProps) => { 'group toast group-[.toaster]:!bg-destructive-200 group-[.toaster]:!border-destructive-500', closeButton: cn( // unset all styles set from sonner - 'absolute right-2 top-2 size-6 flex items-center justify-center rounded-md text-foreground-light opacity-0 transition', - 'hover:text-foreground hover:bg-surface-200 focus:opacity-100 focus:outline-hidden focus:ring-2 group-hover:opacity-100', + 'absolute right-2 top-2 size-6 flex items-center justify-center rounded-md text-foreground-light opacity-0 transition-opacity', + 'hover:text-foreground hover:bg-surface-200 focus-visible:opacity-100 focus-ring group-hover:opacity-100', 'group-[.destructive]:text-destructive-300 group-[.destructive]:hover:text-destructive-50', - 'group-[.destructive]:focus:ring-destructive-400 group-[.destructive]:focus:ring-offset-destructive-600', + 'group-[.destructive]:focus-visible:ring-destructive-400 group-[.destructive]:focus-visible:ring-offset-destructive-600', 'left-auto transform-none border-0 border-transparent' ), content: 'grow', From b76d04d6a0ebc272a8587cc691294e3025830e3a Mon Sep 17 00:00:00 2001 From: Hieu Date: Thu, 23 Jul 2026 12:17:45 +0700 Subject: [PATCH 005/141] fix: read FGA permissions from openapi spec x-fga-permissions extension (#48181) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Follow up to https://github.com/supabase/platform/pull/35940, which moved FGA permissions off security and onto the `x-fga-permissions` extension. This updates the docs reference component to read from the new field. ## Summary by CodeRabbit * **New Features** * API documentation now supports displaying fine-grained access permissions for endpoints. * Endpoint security details are presented more consistently using the documented permissions configuration. --- apps/docs/features/docs/Reference.api.utils.ts | 1 + apps/docs/features/docs/Reference.sections.tsx | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/docs/features/docs/Reference.api.utils.ts b/apps/docs/features/docs/Reference.api.utils.ts index 480da7d88d4..2d87c920508 100644 --- a/apps/docs/features/docs/Reference.api.utils.ts +++ b/apps/docs/features/docs/Reference.api.utils.ts @@ -26,6 +26,7 @@ export interface IApiEndPoint { security?: Array 'x-oauth-scope'?: string 'x-allowed-plans'?: string[] + 'x-fga-permissions'?: string[][] } export type ISchema = diff --git a/apps/docs/features/docs/Reference.sections.tsx b/apps/docs/features/docs/Reference.sections.tsx index 98654285c45..cbced0f844c 100644 --- a/apps/docs/features/docs/Reference.sections.tsx +++ b/apps/docs/features/docs/Reference.sections.tsx @@ -275,10 +275,7 @@ async function ApiEndpointSection({ link, section, servicePath }: ApiEndpointSec : await getApiEndpointById(section.id) if (!endpointDetails) return null - const endpointFgaPermissionGroups = - endpointDetails.security - ?.filter((sec) => 'fga_permissions' in sec) - .map((sec) => sec.fga_permissions) ?? [] + const endpointFgaPermissionGroups = endpointDetails['x-fga-permissions'] ?? [] const pathParameters = (endpointDetails.parameters ?? []).filter((param) => param.in === 'path') const queryParameters = (endpointDetails.parameters ?? []).filter((param) => param.in === 'query') const bodyParameters = From 63e2eb3ca6e98f4285c592c0a32a4a7fd497f194 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 23 Jul 2026 15:19:54 +0800 Subject: [PATCH 006/141] Joshen/fe 3971 blocked by visualization (#48187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Improving the "blocked by" visualisation for database connections - to accommodate the situation whereby there might be a chain of blocked process. Intention is so that users can identify whats the root process that's blocking everything - and from there decide if they want to terminate the process or not. Also brings `ActivityRow` out into its separate file since `Activity` is getting big image ## Summary by CodeRabbit * **Refactor** * Streamlined the database activity view by separating the single-row rendering into its own component, keeping the same end-user experience (status badge, query/“No query”, duration warnings, blocking details, PID copy, and actions). * Kept “Terminate” behind confirmation prompts and preserved role-based restrictions for when termination is available. * **Improvements** * Standardized how activity durations are calculated and how status badges are styled for consistent display. --- .../DatabaseConnections/Activity.tsx | 298 +-------------- .../DatabaseConnections/ActivityRow.tsx | 338 ++++++++++++++++++ .../DatabaseConnections.utils.ts | 27 ++ 3 files changed, 370 insertions(+), 293 deletions(-) create mode 100644 apps/studio/components/interfaces/Observability/DatabaseConnections/ActivityRow.tsx create mode 100644 apps/studio/components/interfaces/Observability/DatabaseConnections/DatabaseConnections.utils.ts diff --git a/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx b/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx index 3c1ed319c43..53a00edfb1d 100644 --- a/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx +++ b/apps/studio/components/interfaces/Observability/DatabaseConnections/Activity.tsx @@ -1,81 +1,16 @@ -import dayjs from 'dayjs' import { isEqual } from 'lodash' -import { Minus, MoreVertical, StopCircle, X } from 'lucide-react' +import { X } from 'lucide-react' import { parseAsArrayOf, parseAsInteger, parseAsString, useQueryState, useQueryStates } from 'nuqs' -import { Fragment, useEffect, useState } from 'react' -import { toast } from 'sonner' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - Badge, - Button, - Card, - cn, - copyToClipboard, - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, - HoverCard, - HoverCardContent, - HoverCardTrigger, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, - Tooltip, - TooltipContent, - TooltipTrigger, -} from 'ui' -import { CodeBlock } from 'ui-patterns/CodeBlock' +import { useEffect } from 'react' +import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { ReportsSelectFilter } from '../../Reports/v2/ReportsSelectFilter' -import { - QUERY_STATE_TOOLTIP, - WARN_DURATION_ACTIVE_QUERY, - WARN_DURATION_IDLE_TXN, -} from './DatabaseConnections.constants' -import { formatDuration } from '@/components/interfaces/QueryPerformance/QueryPerformance.utils' +import { ActivityRow } from './ActivityRow' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' -import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' -import { InlineLinkClassName } from '@/components/ui/InlineLink' import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query' -import { useDatabaseActivityQuery, type DatabaseActivity } from '@/data/database/activity-query' -import { useQueryAbortMutation } from '@/data/sql/abort-query-mutation' +import { useDatabaseActivityQuery } from '@/data/database/activity-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { formatSql } from '@/lib/formatSql' - -const getDuration = (activity: DatabaseActivity) => { - const { state } = activity - if (state === 'active' && activity.query_start) { - return dayjs().utc().diff(dayjs(activity.query_start).utc(), 'second') - } - if (state === 'idle' && activity.state_change) { - return dayjs().utc().diff(dayjs(activity.state_change).utc(), 'second') - } - if ( - (state === 'idle in transaction' || state === 'idle in transaction (aborted)') && - activity.transaction_start - ) { - return dayjs().utc().diff(dayjs(activity.transaction_start).utc(), 'second') - } - return null -} - -const getBadgeVariant = (activity: DatabaseActivity) => { - const { state } = activity - if (state === 'active') return 'success' - if (state === 'idle in transaction' || state === 'idle in transaction (aborted)') return 'warning' - return 'default' -} const DEFAULT_ROLES_FILTER = ['anon', 'authenticated', 'postgres'] @@ -312,226 +247,3 @@ export const Activity = ({ live }: ActivityProps) => {
    ) } - -const ActivityRow = ({ activity }: { activity: DatabaseActivity }) => { - const { data: project } = useSelectedProjectQuery() - const [showTerminateConfirmDialog, setShowTerminateConfirmDialog] = useState(false) - const [selectedPid, setSelectedPid] = useQueryState('pid', parseAsInteger) - - const { data } = useDatabaseActivityQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const { data: roles } = useDatabaseRolesQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const superuserRoles = roles?.filter((role) => role.isSuperuser).map((role) => role.name) - - const { mutateAsync: abortQuery } = useQueryAbortMutation({ - onSuccess: () => { - toast.success(`Successfully aborted query (ID: ${activity.pid})`) - }, - }) - - const durationSeconds = getDuration(activity) - const badgeVariant = getBadgeVariant(activity) - - /** - * Queries in "active state": 30s threshold is long enough (most CRUD queries should be quick) - * Queries in "idle in transaction" state: This actively holds locks and blocks autovacuum while contributing nothing, so important to surface early at 10s threshold - */ - const queryRunningLongWarning = - !!durationSeconds && - ((activity.state === 'active' && durationSeconds >= WARN_DURATION_ACTIVE_QUERY) || - ((activity.state === 'idle in transaction' || - activity.state === 'idle in transaction (aborted)') && - durationSeconds >= WARN_DURATION_IDLE_TXN)) - - const onConfirmTerminate = async () => { - try { - await abortQuery({ - pid: activity.pid, - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - } catch (error) {} - } - - return ( - <> - - - {selectedPid === activity.pid && ( -
    - )} - - - {activity.state} - - {activity.state && ( - {QUERY_STATE_TOOLTIP[activity.state]} - )} - -
    - - - -

    - {!!activity.query ? activity.query : 'No query'} -

    -
    - {activity.query && ( - - code]:text-xs max-h-64', - '[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11' - )} - wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-0')} - language="pgsql" - value={formatSql(activity.query)} - /> - - )} -
    -
    - - { - toast.success('Copied PID') - copyToClipboard(activity.pid.toString()) - }} - > - PID: {activity.pid} - - Click to copy - - · - {activity.role_name} - {activity.application_name && ( - <> - · - {activity.application_name} - - )} -
    -
    - - -

    - {durationSeconds !== null ? ( - formatDuration(durationSeconds * 1000, 0) - ) : ( - - )} -

    -
    - - - {activity.blocked_by.length > 0 ? ( - activity.blocked_by.map((pid, index) => { - const blockedProcess = data?.find((x) => x.pid === pid) - - return ( - - {index > 0 && ', '} - - setSelectedPid(pid)} - > - {pid} - - -

    - {blockedProcess?.query} -

    -

    - PID: {blockedProcess?.pid} - · - {blockedProcess?.role_name} - {blockedProcess?.application_name && ( - <> - · - {blockedProcess?.application_name} - - )} -

    -
    -
    -
    - ) - }) - ) : ( - - )} -
    - - - - -
    -
    -

    Filter by tags

    +
    +

    + Change types +

    + {CHANGE_TYPES.map((type) => { + const { label } = CHANGE_TYPE_DISPLAY[type] + const on = selectedTypes.has(type) + return ( + + ) + })} +
    +
    +
    +
    +

    + Products +

    +
    {CHANGELOG_PRODUCT_TAGS.map(({ slug, label }) => { const on = selectedTags.has(slug) + return ( + + ) + })} +
    +
    +
    +
    +

    + Product stage +

    +
    + {CHANGELOG_PRODUCT_STAGES.map(({ slug, label }) => { + const on = selectedStages.has(slug) return ( + ) + })} +
    +
    +
    +
    +

    + Self-hosted +

    +
    + {CHANGELOG_SELF_HOSTED_OPTIONS.map(({ slug, label }) => { + const on = selectedSelfHosted.has(slug) + return ( +
    @@ -404,6 +527,21 @@ function ChangelogIndex({ featured, restIndex, allIndex }: PageProps) { )} + {entry.affectedProducts.length > 0 && ( +
    + {entry.affectedProducts.map((product) => ( + + + {product} + + + ))} +
    + )}
    ))} @@ -416,7 +554,7 @@ function ChangelogIndex({ featured, restIndex, allIndex }: PageProps) { )}
    -
    +
    ( +const ChangelogDetailPage = ({ title, created_at, slug, frontmatter, source }: PageProps) => ( <> @@ -54,16 +46,18 @@ const ChangelogDetailPage = ({ title, url, created_at, slug, source, labels }: P

    {title}

    -

    - {dayjs(created_at).format('MMM D, YYYY')} -

    +
    +

    + {dayjs(created_at).format('MMM D, YYYY')} +

    +
    {'error' in source ? ( -

    Error rendering blog post: {source.error.message}

    +

    Error rendering changelog entry: {source.error.message}

    ) : ( )} @@ -72,7 +66,7 @@ const ChangelogDetailPage = ({ title, url, created_at, slug, source, labels }: P
    @@ -88,53 +82,23 @@ export const getStaticPaths: GetStaticPaths = async () => { export const getStaticProps: GetStaticProps = async ({ params }) => { const raw = params?.slug - const slugStr = Array.isArray(raw) ? raw[0] : (raw ?? '') - // The slug always starts with the numeric discussion number. - const number = parseInt(slugStr, 10) - if (!Number.isFinite(number) || number <= 0) return { notFound: true } + const slug = Array.isArray(raw) ? raw[0] : (raw ?? '') - try { - const octokit = createChangelogOctokit() - const discussion = await fetchChangelogDiscussionByNumber( - octokit, - 'supabase', - 'supabase', - number - ) + const entries = await getChangelogEntries() + const entry = entries.find((e) => e.slug === slug) + if (!entry) return { notFound: true } - if (!discussion || discussion.category?.id !== CHANGELOG_CATEGORY_ID) { - return { notFound: true } - } + const source = await mdxSerialize(entry.bodySection) - const expectedSlug = changelogEntrySlug(number, discussion.title) - - // Redirect number-only or mismatched slugs to the canonical slug URL. - if (slugStr !== expectedSlug) { - return { redirect: { destination: `/changelog/${expectedSlug}`, permanent: true } } - } - - const source = await mdxSerialize(discussion.body) - const created_at = - discussionDisplayDate({ - title: discussion.title, - createdAt: discussion.createdAt, - }) ?? discussion.createdAt - - return { - props: { - title: discussion.title, - url: discussion.url, - created_at, - number, - slug: expectedSlug, - source, - labels: discussion.labels?.nodes ?? [], - }, - revalidate: 900, - } - } catch (e) { - console.error(e) - return { notFound: true } + return { + props: { + title: entry.frontmatter.title, + created_at: entry.sortDate, + slug: entry.slug, + frontmatter: entry.frontmatter, + source, + }, + revalidate: 900, } } diff --git a/apps/www/scripts/data/changelog-deleted-discussions.json b/apps/www/scripts/data/changelog-deleted-discussions.json deleted file mode 100644 index d4d97616816..00000000000 --- a/apps/www/scripts/data/changelog-deleted-discussions.json +++ /dev/null @@ -1,21 +0,0 @@ -[ - { "title": "Platform updates: October 2023", "createdAt": "2023-11-06T16:25:12Z" }, - { "title": "Platform update September 2023", "createdAt": "2023-10-06T09:23:56Z" }, - { "title": "Platform updates: August 2023", "createdAt": "2023-09-08T13:00:39Z" }, - { "title": "Platform updates June 2023", "createdAt": "2023-07-07T16:09:32Z" }, - { "title": "Platform updates: May 2023", "createdAt": "2023-06-09T16:40:16Z" }, - { "title": "Platform updates: April 2023", "createdAt": "2023-05-10T18:40:02Z" }, - { "title": "Platform Update February 2023", "createdAt": "2023-03-09T12:06:01Z" }, - { "title": "Platform update January 2023", "createdAt": "2023-02-08T18:29:41Z" }, - { "title": "Platform Updates November 2022", "createdAt": "2022-12-08T12:00:48Z" }, - { "title": "Platform updates: October 2022", "createdAt": "2022-11-02T16:07:47Z" }, - { "title": "Platform Update September 2022", "createdAt": "2022-10-07T10:18:21Z" }, - { "title": "Platform updates: 30 Nov 2021", "createdAt": "2021-11-30T10:06:55Z" }, - { "title": "October Beta 2021", "createdAt": "2021-11-08T13:14:35Z" }, - { "title": "September Beta 2021", "createdAt": "2021-10-04T18:10:57Z" }, - { "title": "August Beta 2021", "createdAt": "2021-09-13T09:47:18Z" }, - { "title": "July Beta 2021", "createdAt": "2021-08-12T13:46:14Z" }, - { "title": "June Beta 2021", "createdAt": "2021-07-04T13:23:35Z" }, - { "title": "May Beta 2021", "createdAt": "2021-06-10T11:53:14Z" }, - { "title": "April Beta 2021", "createdAt": "2021-05-05T05:17:27Z" } -] diff --git a/apps/www/scripts/generateStaticContent.mjs b/apps/www/scripts/generateStaticContent.mjs index 0fac6a1eaed..04f694b38c0 100644 --- a/apps/www/scripts/generateStaticContent.mjs +++ b/apps/www/scripts/generateStaticContent.mjs @@ -378,198 +378,140 @@ try { console.warn('Error generating RSS feed:', error) } -// Changelog RSS → public/changelog-rss.xml (same GitHub App + category as lib/changelog-github.ts) -try { - const appId = process.env.GITHUB_CHANGELOG_APP_ID - const installationId = process.env.GITHUB_CHANGELOG_APP_INSTALLATION_ID - const privateKey = process.env.GITHUB_CHANGELOG_APP_PRIVATE_KEY +// Changelog RSS + changelog.md → sourced from supabase/changelog entries/*.md (private repo). +// Missing secret: warns and skips outside Vercel, but fails Vercel builds so they can't +// publish stale generated changelog files. Generic CI (typecheck/lint on GitHub Actions) +// has no access to this secret and isn't publishing anything, so it only warns too. +async function generateChangelogContent() { + const appId = process.env.CHANGELOG_SYNC_APP_ID + const installationId = process.env.CHANGELOG_SYNC_APP_INSTALLATION_ID + const privateKey = process.env.CHANGELOG_SYNC_APP_PRIVATE_KEY if (!appId || !installationId || !privateKey) { - console.warn('Skipping changelog RSS: missing GITHUB_CHANGELOG_APP_* env vars') - } else { - const { createAppAuth } = await import('@octokit/auth-app') - const { Octokit } = await import('@octokit/core') - const { paginateGraphql } = await import('@octokit/plugin-paginate-graphql') - - const { generateChangelogRssXml, generateChangelogTagRssXml, labelToFileSlug, changelogEntrySlug } = - await import('../lib/changelog-rss.mjs') - const rewritesPath = path.join(__dirname, 'data/changelog-deleted-discussions.json') - const rewrites = JSON.parse(await fs.readFile(rewritesPath, 'utf8')) - const discussionDisplayDate = (item) => { - const dateRewrite = rewrites.find( - (r) => item.title && r.title && item.title.includes(r.title) - ) - return dateRewrite ? dateRewrite.createdAt : item.createdAt + if (process.env.VERCEL) { + throw new Error('CHANGELOG_SYNC_APP_* env vars not set — cannot generate changelog content') } + console.warn('⚠️ CHANGELOG_SYNC_APP_* env vars not set — skipping changelog RSS/md generation') + return + } - const CHANGELOG_CATEGORY_ID = 'DIC_kwDODMpXOc4CAFUr' + const { getPublishedChangelogEntries, fetchChangelogEntryFilesFromTarball, CHANGE_TYPE_LABELS } = + await import('../lib/changelog-entries-core.mjs') + const { generateChangelogRssXml, generateChangelogTagRssXml, labelToFileSlug } = await import( + '../lib/changelog-rss.mjs' + ) + const { createAppAuth } = await import('@octokit/auth-app') + const { Octokit } = await import('@octokit/core') + const octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { appId, installationId, privateKey: privateKey.replace(/\\n/g, '\n') }, + }) - const changelogQuery = ` - query changelogDiscussionMetadata($cursor: String, $owner: String!, $repo: String!, $categoryId: ID!) { - repository(owner: $owner, name: $repo) { - discussions( - first: 100 - after: $cursor - categoryId: $categoryId - orderBy: { field: CREATED_AT, direction: DESC } - ) { - nodes { - number - title - body - createdAt - url - labels(first: 25) { - nodes { - name - } - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - ` + // Single tarball request — fetching each entry file individually trips + // GitHub's secondary rate limit once the entries directory gets large. + const files = await fetchChangelogEntryFilesFromTarball(octokit, { + owner: 'supabase', + repo: 'changelog', + entriesPath: 'entries', + }) + const entries = getPublishedChangelogEntries(files) - const ExtendedOctokit = Octokit.plugin(paginateGraphql) - const octokit = new ExtendedOctokit({ - authStrategy: createAppAuth, - auth: { - appId, - installationId, - privateKey: privateKey.replace(/\\n/g, '\n'), - }, + const rssEntries = entries.map((entry) => ({ + slug: entry.slug, + title: entry.frontmatter.title, + sortDate: entry.sortDate, + affectedProducts: entry.frontmatter.affected_products ?? [], + })) + + const changelogXml = generateChangelogRssXml(rssEntries) + const changelogRssPath = path.join(__dirname, '../public/changelog-rss.xml') + await fs.writeFile(changelogRssPath, changelogXml.trim(), 'utf8') + console.log(`✅ Generated changelog RSS with ${entries.length} entries`) + + // Per-tag feeds → public/changelog-rss/.xml + const productTagsPath = path.join(__dirname, '../data/changelog-product-tags.json') + const productTags = JSON.parse(await fs.readFile(productTagsPath, 'utf8')) + const tagFeedsDir = path.join(__dirname, '../public/changelog-rss') + // Clear first so a renamed/removed product tag doesn't leave a stale feed file behind. + await fs.rm(tagFeedsDir, { recursive: true, force: true }) + await fs.mkdir(tagFeedsDir, { recursive: true }) + const tagFilenames = productTags.map(({ label }) => `${labelToFileSlug(label)}.xml`) + const tagResults = await Promise.allSettled( + productTags.map(async ({ label }) => { + const fileSlug = labelToFileSlug(label) + const tagXml = generateChangelogTagRssXml(rssEntries, { displayLabel: label }) + await fs.writeFile(path.join(tagFeedsDir, `${fileSlug}.xml`), tagXml.trim(), 'utf8') }) - - const collected = [] - let cursor = null - let hasNextPage = true - while (hasNextPage) { - const { - repository: { - discussions: { nodes, pageInfo }, - }, - } = await octokit.graphql(changelogQuery, { - owner: 'supabase', - repo: 'supabase', - categoryId: CHANGELOG_CATEGORY_ID, - cursor, - }) - collected.push(...nodes) - hasNextPage = pageInfo.hasNextPage - cursor = pageInfo.endCursor + ) + const failedTagFeeds = tagResults.flatMap((result, i) => + result.status === 'rejected' ? [{ file: tagFilenames[i], reason: result.reason }] : [] + ) + const succeeded = tagResults.length - failedTagFeeds.length + console.log(`✅ Generated ${succeeded}/${productTags.length} per-tag changelog RSS feeds`) + if (failedTagFeeds.length > 0) { + for (const { file, reason } of failedTagFeeds) { + console.error(`Failed to write changelog-rss/${file}:`, reason) } - - const entries = collected.map((item) => ({ - number: item.number, - slug: changelogEntrySlug(item.number, item.title), - title: item.title, - url: item.url, - sortDate: discussionDisplayDate({ title: item.title, createdAt: item.createdAt }), - labels: (item.labels?.nodes ?? []).map((l) => l.name.toLowerCase()), - body: item.body ?? '', - })) - - const changelogXml = generateChangelogRssXml(entries) - const changelogRssPath = path.join(__dirname, '../public/changelog-rss.xml') - await fs.writeFile(changelogRssPath, changelogXml.trim(), 'utf8') - const visibleCount = entries.filter((e) => !e.title.includes('[d]')).length - console.log(`✅ Generated changelog RSS with ${visibleCount} entries`) - - // Per-tag feeds → public/changelog-rss/.xml - const productTagsPath = path.join(__dirname, '../data/changelog-product-tags.json') - const productTags = JSON.parse(await fs.readFile(productTagsPath, 'utf8')) - const tagFeedsDir = path.join(__dirname, '../public/changelog-rss') - await fs.mkdir(tagFeedsDir, { recursive: true }) - const tagResults = await Promise.allSettled( - productTags.map(async ({ slug, label }) => { - const fileSlug = labelToFileSlug(label) - const tagXml = generateChangelogTagRssXml(entries, { - githubLabelSlug: slug, - displayLabel: label, - }) - await fs.writeFile(path.join(tagFeedsDir, `${fileSlug}.xml`), tagXml.trim(), 'utf8') - }) + throw new Error( + `Failed to generate ${failedTagFeeds.length}/${productTags.length} per-tag changelog RSS feeds` ) - const succeeded = tagResults.filter((r) => r.status === 'fulfilled').length - console.log(`✅ Generated ${succeeded}/${productTags.length} per-tag changelog RSS feeds`) + } - // LLM-friendly changelog markdown index (RSS remains canonical syndication format). - const visibleEntries = entries.filter((entry) => !entry.title.includes('[d]')) + // LLM-friendly changelog markdown index (RSS remains canonical syndication format). + const mdSections = entries.map((entry) => { + const date = dayjs(entry.sortDate).isValid() ? dayjs(entry.sortDate).format('YYYY-MM-DD') : '' + const changeType = CHANGE_TYPE_LABELS[entry.frontmatter.change_type] ?? entry.frontmatter.change_type + const products = (entry.frontmatter.affected_products ?? []).join(', ') + const meta = [ + date, + changeType, + products, + `[supabase.com/changelog/${entry.slug}](https://supabase.com/changelog/${entry.slug})`, + ] + .filter(Boolean) + .join(' · ') + return [`## ${entry.frontmatter.title}`, meta, entry.summary].filter(Boolean).join('\n\n') + }) + const changelogMd = `# Supabase Changelog\n\n${mdSections.join('\n\n---\n\n')}\n` + const changelogMdPath = path.join(__dirname, '../public/changelog.md') + await fs.writeFile(changelogMdPath, changelogMd, 'utf8') + console.log(`✅ Generated changelog.md (${entries.length} entries)`) - /** - * Extracts the first meaningful paragraph from a markdown body. - * Skips headings, code fences, HTML blocks, and empty lines. - */ - const extractSummary = (body) => { - if (!body) return '' - for (const para of body.split(/\n{2,}/)) { - const trimmed = para.trim() - if ( - !trimmed || - trimmed.startsWith('#') || - trimmed.startsWith('```') || - trimmed.startsWith('<') || - trimmed.startsWith('|') || - trimmed.startsWith('---') - ) continue - const oneLiner = trimmed.replace(/\n/g, ' ') - return oneLiner.length > 200 ? oneLiner.slice(0, 200).replace(/\s+\S*$/, '') + '…' : oneLiner - } - return '' - } - - const mdSections = visibleEntries.map((entry) => { - const date = dayjs(entry.sortDate).isValid() ? dayjs(entry.sortDate).format('YYYY-MM-DD') : '' - const labels = (entry.labels ?? []).join(', ') - const meta = [date, labels, `[supabase.com/changelog/${entry.slug}](https://supabase.com/changelog/${entry.slug})`] - .filter(Boolean) - .join(' · ') - const summary = extractSummary(entry.body) - return [`## ${entry.title}`, meta, summary].filter(Boolean).join('\n\n') - }) - const changelogMd = `# Supabase Changelog\n\n${mdSections.join('\n\n---\n\n')}\n` - const changelogMdPath = path.join(__dirname, '../public/changelog.md') - await fs.writeFile(changelogMdPath, changelogMd, 'utf8') - console.log(`✅ Generated changelog.md (${visibleEntries.length} entries)`) - - // One markdown file per entry → /changelog/.md (same content shape as the web page body). - const changelogEntryMdDir = path.join(__dirname, '../public/changelog') - await fs.mkdir(changelogEntryMdDir, { recursive: true }) - for (const entry of visibleEntries) { - const published = dayjs(entry.sortDate).isValid() - ? dayjs(entry.sortDate).format('YYYY-MM-DD') - : '' - const titleLine = String(entry.title ?? '') - .replace(/\n/g, ' ') - .trim() - const labelsYaml = (entry.labels ?? []).map((l) => ` - ${l}`).join('\n') - const pageUrl = `https://supabase.com/changelog/${entry.slug}` - const entryMd = `--- -number: ${entry.number} + // One markdown file per entry → /changelog/.md (Body section only — internal notes never included). + const changelogEntryMdDir = path.join(__dirname, '../public/changelog') + // Clear first so a renamed/unpublished entry doesn't leave a stale file behind. + await fs.rm(changelogEntryMdDir, { recursive: true, force: true }) + await fs.mkdir(changelogEntryMdDir, { recursive: true }) + for (const entry of entries) { + const published = dayjs(entry.sortDate).isValid() + ? dayjs(entry.sortDate).format('YYYY-MM-DD') + : '' + const titleLine = String(entry.frontmatter.title ?? '') + .replace(/\n/g, ' ') + .trim() + const productsYaml = (entry.frontmatter.affected_products ?? []) + .map((p) => ` - ${p}`) + .join('\n') + const pageUrl = `https://supabase.com/changelog/${entry.slug}` + const entryMd = `--- slug: ${entry.slug} published: ${published} -discussion: ${entry.url} -labels: -${labelsYaml || ' []'} +change_type: ${entry.frontmatter.change_type} +affected_products: +${productsYaml || ' []'} page: ${pageUrl} --- # ${titleLine} -${entry.body ?? ''} +${entry.bodySection} ` - await fs.writeFile( - path.join(changelogEntryMdDir, `${entry.slug}.md`), - entryMd.trim() + '\n', - 'utf8' - ) - } - console.log(`✅ Generated changelog/*.md (${visibleEntries.length} files)`) + await fs.writeFile( + path.join(changelogEntryMdDir, `${entry.slug}.md`), + entryMd.trim() + '\n', + 'utf8' + ) } -} catch (error) { - console.warn('Error generating changelog RSS:', error) + console.log(`✅ Generated changelog/*.md (${entries.length} files)`) } +await generateChangelogContent() diff --git a/apps/www/turbo.jsonc b/apps/www/turbo.jsonc index e4ba1802468..728b752ffc1 100644 --- a/apps/www/turbo.jsonc +++ b/apps/www/turbo.jsonc @@ -57,6 +57,9 @@ "GITHUB_CHANGELOG_APP_INSTALLATION_ID", "GITHUB_CHANGELOG_APP_REST_KEY", "GITHUB_CHANGELOG_APP_PRIVATE_KEY", + "CHANGELOG_SYNC_APP_ID", + "CHANGELOG_SYNC_APP_INSTALLATION_ID", + "CHANGELOG_SYNC_APP_PRIVATE_KEY", "NEXT_PUBLIC_EMAIL_ABUSE_URL", "EMAIL_ABUSE_SERVICE_KEY", "HUBSPOT_PORTAL_ID", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 783197513ee..95702943339 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1932,6 +1932,9 @@ importers: tailwindcss: specifier: 'catalog:' version: 4.2.4 + tar-stream: + specifier: ^3.1.7 + version: 3.1.7 tsconfig: specifier: workspace:* version: link:../../packages/tsconfig From d900c09e8a0314ecfef2f9dbc0e98a986704ff73 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 23 Jul 2026 11:43:22 +0200 Subject: [PATCH 012/141] chore(studio): grafana-cloud slug (#48238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use `grafana-cloud` slug for the partner integration. ## Summary by CodeRabbit * **New Features** * Added Grafana Cloud to the featured integrations in the marketplace. * Added light and dark themed artwork for the Grafana Cloud featured integration. * **Improvements** * Updated the OAuth “installed” detection to include Grafana Cloud alongside Grafana. * Included Grafana Cloud in the set of official partners for the partner-only filter. * **Tests** * Expanded marketplace integration fixtures to cover Grafana Cloud scenarios. --- .../Integrations/Landing/Landing.utils.ts | 2 +- .../Landing/MarketplaceDetail.test.tsx | 15 +++++++++++++++ .../Marketplace/Marketplace.constants.tsx | 1 + .../Marketplace/MarketplaceFeaturedHeroGrid.tsx | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Integrations/Landing/Landing.utils.ts b/apps/studio/components/interfaces/Integrations/Landing/Landing.utils.ts index cd011a216db..9315eef4ba0 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Landing.utils.ts +++ b/apps/studio/components/interfaces/Integrations/Landing/Landing.utils.ts @@ -169,7 +169,7 @@ export const isOAuthInstalled = ({ ) } - if (integration.id === 'grafana') { + if (integration.id === 'grafana' || integration.id === 'grafana-cloud') { // Grafana is not yet sending integration status, so just use presence of API key. return ( isOAuthAppAuthorized(projectData, integration) || diff --git a/apps/studio/components/interfaces/Integrations/Landing/MarketplaceDetail.test.tsx b/apps/studio/components/interfaces/Integrations/Landing/MarketplaceDetail.test.tsx index 5ab944ae5b1..681f22bd809 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/MarketplaceDetail.test.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/MarketplaceDetail.test.tsx @@ -48,6 +48,21 @@ const STABLE_INTEGRATIONS = [ navigate: () => null, navigation: [{ route: 'overview', label: 'Overview' }], }, + { + id: 'grafana-cloud', + name: 'Grafana', + type: 'oauth', + source: 'Partner', + description: 'Grafana', + content: 'Grafana overview content', + docsUrl: null, + siteUrl: null, + author: { name: 'Grafana Labs' }, + oauthAppId: 'grafana-app', + icon: () => null, + navigate: () => null, + navigation: [{ route: 'overview', label: 'Overview' }], + }, ] const STABLE_EMPTY_ARR = [] as unknown[] vi.mock('@/components/interfaces/Integrations/Landing/useAvailableIntegrations', () => ({ diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx index 5c8c8b097bd..2d64bb82d65 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx @@ -14,6 +14,7 @@ export type { MarketplaceSource } from '@/components/interfaces/Integrations/Lan // Defines featured integrations and their order in the featured hero export const FEATURED_INTEGRATION_IDS = [ 'grafana', + 'grafana-cloud', 'cron', 'queues', 'stripe_sync_engine', diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx index 8a0fe9880fe..1714b1f6e52 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx @@ -25,6 +25,10 @@ const FEATURED_INTEGRATION_IMAGES: Record Date: Thu, 23 Jul 2026 12:38:10 +0200 Subject: [PATCH 013/141] fix: fix migration dialog does not show the correct content in some edge cases (#48239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem When two projects have migrations with the same version but different content, the details panel does not update the content and shows the first loaded migration one. ## Solution This is because the CodeEditor does not react to content only changes. Settings its `key` ensures it does. Unfortunately, we can't unit test that the CodeEditor content changes correctly. ## How to test - create two projects and push a migration with the same version but different content on them - open the _Database/Migrations_ page for the first project - click the _View migration SQL_ and ensure its content matches the migration for this project - select the other project using the top bar - click the _View migration SQL_ and ensure its content matches the migration for this project ## Summary by CodeRabbit * **Bug Fixes** * Improved migration SQL display when switching between migrations or projects. * Ensured the code editor consistently refreshes with the currently selected migration’s statements. * Improved type safety for the migration search input. --- .../interfaces/Database/Migrations/Migrations.tsx | 12 +++++++----- .../pages/project/[ref]/database/migrations.tsx | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx index 24f4763fa21..b893c62e324 100644 --- a/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx +++ b/apps/studio/components/interfaces/Database/Migrations/Migrations.tsx @@ -32,7 +32,7 @@ import { formatMigrationVersionLabel, parseMigrationVersion } from '@/lib/migrat import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' -const Migrations = () => { +export const Migrations = () => { const [search, setSearch] = useState('') const [selectedMigration, setSelectedMigration] = useState() const searchInputRef = useRef(null) @@ -116,7 +116,7 @@ const Migrations = () => { placeholder="Search for a migration" value={search} className="w-full lg:w-52" - onChange={(e: any) => setSearch(e.target.value)} + onChange={(e) => setSearch(e.target.value)} icon={} /> @@ -221,10 +221,14 @@ const Migrations = () => {
    { ) } - -export default Migrations diff --git a/apps/studio/pages/project/[ref]/database/migrations.tsx b/apps/studio/pages/project/[ref]/database/migrations.tsx index e57b15dd6fd..425a6ec8762 100644 --- a/apps/studio/pages/project/[ref]/database/migrations.tsx +++ b/apps/studio/pages/project/[ref]/database/migrations.tsx @@ -9,7 +9,7 @@ import { } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' -import Migrations from '@/components/interfaces/Database/Migrations/Migrations' +import { Migrations } from '@/components/interfaces/Database/Migrations/Migrations' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { DocsButton } from '@/components/ui/DocsButton' From 81085286828925562344c3d2188155d64f856fd4 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 23 Jul 2026 12:48:33 +0200 Subject: [PATCH 014/141] Fix: Update the auth user field in the Logs page (#48237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix the user filter to work with `edge_logs`. - Update the `auth_user` field to be derived from other log attributes. - Removed filtering for `postgres_logs` since it didn't really filter by user actions, only by user id mentions. ## Summary by CodeRabbit - **Bug Fixes** - Improved unified logs user filtering to rely only on exact attribution identifiers from authentication and edge log sources, removing partial message-based matching. - Updated unified logs user identification by deriving `auth_user` from authentication actor IDs or edge JWT subject values. - Refined “user filter reachability” logic to consider only attributable log types (auth and edge). - **Tests** - Adjusted unified logs query tests to match the updated attribution routing and reachability outcomes. --- .../UnifiedLogs/UnifiedLogs.queries.test.ts | 24 +++++++++++-------- .../UnifiedLogs/UnifiedLogs.queries.ts | 23 +++++++++++------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts index 15f713a3474..a9b62e271bd 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts @@ -282,14 +282,18 @@ describe('UnifiedLogs.queries (OTEL flat)', () => { }) describe('user filter', () => { - it('restricts to auth_logs/postgres_logs and skips the default postgres+edge restriction', () => { + it('restricts to auth_logs/edge_logs and skips the default postgres+edge restriction', () => { const sql = getUnifiedLogsQuery(withUser('user-123')) const where = sql.split(/\bWHERE\b/)[1] ?? '' - expect(where).toContain(`log_attributes['auth_event.actor_id'] = 'user-123'`) - expect(where).toContain(`source = 'postgres_logs'`) - // The unfiltered default (postgres_logs OR edge_logs) would incorrectly exclude + expect(where).toContain( + `(source = 'auth_logs' AND log_attributes['auth_event.actor_id'] = 'user-123')` + ) + expect(where).toContain( + `(source = 'edge_logs' AND log_attributes['request.sb.jwt.authorization.payload.subject'] = 'user-123')` + ) + // The unfiltered default (edge_logs OR postgres_logs) would incorrectly exclude // auth_logs, the primary attributable source, so it must not appear here. - expect(where).not.toContain(`(source = 'postgres_logs') OR (source = 'edge_logs')`) + expect(where).not.toContain(`(source = 'edge_logs') OR (source = 'postgres_logs')`) }) it('does not restrict sources when the user filter is inactive', () => { @@ -321,8 +325,8 @@ describe('UnifiedLogs.queries (OTEL flat)', () => { expect(isUserFilterUnreachable(withUser('user-123', 'log_type:eq:auth'))).toBe(false) }) - it('is false when the explicit log_type filter includes an attributable source (postgres)', () => { - expect(isUserFilterUnreachable(withUser('user-123', 'log_type:eq:postgres'))).toBe(false) + it('is false when the explicit log_type filter includes an attributable source (edge)', () => { + expect(isUserFilterUnreachable(withUser('user-123', 'log_type:eq:edge'))).toBe(false) }) it('is false when at least one of several selected log types is attributable', () => { @@ -332,12 +336,12 @@ describe('UnifiedLogs.queries (OTEL flat)', () => { }) it('is true when the explicit log_type filter restricts to a single non-attributable source', () => { - expect(isUserFilterUnreachable(withUser('user-123', 'log_type:eq:edge'))).toBe(true) + expect(isUserFilterUnreachable(withUser('user-123', 'log_type:eq:storage'))).toBe(true) }) it('is true when every selected log type is non-attributable', () => { expect( - isUserFilterUnreachable(withUser('user-123', 'log_type:eq:edge', 'log_type:eq:storage')) + isUserFilterUnreachable(withUser('user-123', 'log_type:eq:realtime', 'log_type:eq:storage')) ).toBe(true) }) @@ -347,7 +351,7 @@ describe('UnifiedLogs.queries (OTEL flat)', () => { it('(neq) is true only when both attributable sources are excluded', () => { expect( - isUserFilterUnreachable(withUser('user-123', 'log_type:neq:auth', 'log_type:neq:postgres')) + isUserFilterUnreachable(withUser('user-123', 'log_type:neq:auth', 'log_type:neq:edge')) ).toBe(true) }) }) diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts index 1b56bae859e..619fd33ceef 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts @@ -104,6 +104,13 @@ const LEVEL_EXPR: SafeLogSqlFragment = safeSql`CASE ELSE 'success' END` +// The `auth_user` column is derived from the source-specific fields that can be attributed to a user. +const AUTH_USER_EXPR: SafeLogSqlFragment = safeSql`nullIf(CASE + WHEN source = 'auth_logs' THEN log_attributes['auth_event.actor_id'] + WHEN source = 'edge_logs' THEN log_attributes['request.sb.jwt.authorization.payload.subject'] + ELSE null + END, '')` + const logTypeWhereCondition = (logTypes: string[]): SafeLogSqlFragment => { const effective = logTypes.filter((t) => t in LOG_TYPE_CONDITION) const types = effective.length ? effective : [...DEFAULT_LOG_TYPES] @@ -248,7 +255,7 @@ const truncationFunction = (level: 'MINUTE' | 'HOUR' | 'DAY'): SafeLogSqlFragmen * inlined so the result can be referenced (or filtered) at the same query * level — the OTEL endpoint rejects subqueries. */ -const rowProjection = (): SafeLogSqlFragment => safeSql` +const ROW_PROJECTION: SafeLogSqlFragment = safeSql` id, null AS source_id, timestamp, @@ -258,6 +265,7 @@ const rowProjection = (): SafeLogSqlFragment => safeSql` ${ATTR.path} AS pathname, event_message, ${ATTR.method} AS method, + ${AUTH_USER_EXPR} AS auth_user, null AS log_count, null AS logs ` @@ -335,17 +343,14 @@ const userAttributionCondition = (search: QuerySearchParamsType): SafeLogSqlFrag const value = userFilterValue(search) if (!value) return null const exact = lit(value) - const contains = lit('%' + value + '%') return safeSql`( - (source = 'auth_logs' AND ( - log_attributes['auth_event.actor_id'] = ${exact} - OR event_message ILIKE ${contains} - )) - OR (source = 'postgres_logs' AND event_message ILIKE ${contains}) + (source = 'auth_logs' AND log_attributes['auth_event.actor_id'] = ${exact}) + OR + (source = 'edge_logs' AND log_attributes['request.sb.jwt.authorization.payload.subject'] = ${exact}) )` } -const USER_ATTRIBUTABLE_SOURCES = new Set([LOG_TYPE_TO_SOURCE.auth, LOG_TYPE_TO_SOURCE.postgres]) +const USER_ATTRIBUTABLE_SOURCES = new Set([LOG_TYPE_TO_SOURCE.auth, LOG_TYPE_TO_SOURCE.edge]) /** * True when the user filter is active but an explicit log_type filter restricts the @@ -411,7 +416,7 @@ const applySearchParamsFilter = (search: QuerySearchParamsType): SafeLogSqlFragm export const getUnifiedLogsQuery = (search: QuerySearchParamsType): SafeLogSqlFragment => { const conditions = buildBaseWhere(search) return safeSql`-- unified logs: row list -SELECT ${rowProjection()} +SELECT ${ROW_PROJECTION} FROM logs ${whereClause(conditions)} ` From 4272aba4b16c111b0ce5922a6d5891e35bf4fdb3 Mon Sep 17 00:00:00 2001 From: Jeremias Menichelli Date: Thu, 23 Jul 2026 14:42:07 +0200 Subject: [PATCH 015/141] fix: Update Grafana Cloud integration guides (#47776) --- .../docs/components/MetricsStackCards.data.ts | 5 +-- .../telemetry/metrics/grafana-cloud.mdx | 44 +++++++++++++------ .../telemetry/metrics/grafana-self-hosted.mdx | 8 ++-- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/apps/docs/components/MetricsStackCards.data.ts b/apps/docs/components/MetricsStackCards.data.ts index 1cbd0a4a156..637af231728 100644 --- a/apps/docs/components/MetricsStackCards.data.ts +++ b/apps/docs/components/MetricsStackCards.data.ts @@ -17,10 +17,7 @@ export const metricsStackOptions: MetricsStackOption[] = [ iconKind: 'grafana', iconColor: '#F05A28', iconBg: 'rgba(240,90,40,0.1)', - badges: [ - { label: 'Supabase guide', variant: 'default' }, - { label: 'Community', variant: 'community' }, - ], + badges: [{ label: 'Supabase guide', variant: 'default' }], }, { title: 'Grafana + self-hosted Prometheus', diff --git a/apps/docs/content/guides/telemetry/metrics/grafana-cloud.mdx b/apps/docs/content/guides/telemetry/metrics/grafana-cloud.mdx index 39d96d066ba..42f894f2b87 100644 --- a/apps/docs/content/guides/telemetry/metrics/grafana-cloud.mdx +++ b/apps/docs/content/guides/telemetry/metrics/grafana-cloud.mdx @@ -6,37 +6,55 @@ description: 'Use Grafana Cloud’s managed Prometheus to visualize Supabase met Grafana Cloud gives you a fully managed Prometheus endpoint plus hosted Grafana dashboards, which makes it the fastest way to explore the Supabase Metrics API without operating your own infrastructure. - +## Installation -Grafana maintains a [Supabase integration guide](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-supabase/) for Grafana Cloud. It runs on the same Metrics API documented here, but is community-maintained by Grafana, so feature coverage might differ from what Supabase officially supports. +The Grafana Cloud integration is available in the Supabase Dashboard. [Add the integration](/dashboard/project/_/integrations/grafana-cloud/overview?utm_source=metrics-api&utm_medium=docs&utm_campaign=supabase_grafana_cloud_one_click_integration) and select a project to get a fully-configured instance in one click: authentication, metric scraping, and a pre-built dashboard tracking 200+ metrics, set up automatically. + +By integrating Supabase with Grafana Cloud, users gain monitoring capabilities for Supabase performance and operations. The included dashboard offers a comprehensive overview of Supabase performance, supplemented with Postgres metrics. + +## Manual setup + +The [Grafana Cloud integration](/dashboard/project/_/integrations/grafana-cloud/overview?utm_source=metrics-api&utm_medium=docs&utm_campaign=supabase_grafana_cloud_one_click_integration) available in the Supabase Dashboard is the recommended path. You can still configure and run the setup manually by following the steps below. + + + +Use this guide only if you need full manual control (custom scrape topology, self-hosted +Prometheus or non-standard auth). -## Prerequisites +### Prerequisites - A Supabase project with access to the Metrics API (Secret API key `sb_secret_...`). - A Grafana Cloud account with Prometheus metrics enabled (Free or Pro tier). - A Grafana API token with the `metrics:write` and `metrics:read` scopes if you plan to push data manually. -<$Partial path="metrics_access.mdx" /> - -## 1. Create a Grafana Cloud stack +### 1. Create a Grafana Cloud stack 1. Sign in to [Grafana Cloud](https://grafana.com/auth/sign-in). -2. Create (or select) a stack that has **Prometheus Metrics** enabled (Free and Pro tiers both work for this guide). +2. Create or select a stack that has **Prometheus Metrics** enabled. -## 2. Configure the Supabase integration +### 2. Install the Supabase integration for Grafana Cloud + +1. In your Grafana Cloud stack, click **Connections** in the left-hand menu. +2. Select the **Supabase** integration and follow the steps outlined on the **Configuration** page: +3. Give your scrape job a descriptive name like `production-eu-central-1`. +4. Set your Project ID and enter the service account API key. +5. Test the connection and save the scrape job. +6. Once scrape jobs are configured, click Install to add the prebuilt dashboards to your Grafana Cloud instance. + +### 3. Configure the Supabase integration 1. Navigate to **Connections → Add new connection → Supabase** inside Grafana Cloud. 2. Provide: - Your Supabase project ref (e.g. `abcd1234`). - - The Metrics API endpoint: `https://.supabase.co/customer/v1/privileged/metrics`. + - The Metrics API endpoint (e.g. `https://.supabase.co/customer/v1/privileged/metrics`). - HTTP Basic Auth credentials (`sb_secret_...`). -3. Choose the scrape interval (1 minute recommended) and test the connection. Grafana Cloud will deploy an agent in the background that scrapes the Metrics API and forwards the data to Prometheus. +3. Choose the scrape interval. 1 minute is recommended, and test the connection. Grafana Cloud will deploy an agent in the background that scrapes the Metrics API and forwards the data to Prometheus. If you prefer to reuse an existing Grafana Agent deployment, configure an [integration pipeline](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-supabase/) with the same URL and credentials. -## 3. Import the Supabase dashboard +### 4. Import the Supabase dashboard 1. Open your Grafana Cloud dashboard list and click **New → Import**. 2. Paste the raw contents of [`supabase-grafana/dashboard.json`](https://raw.githubusercontent.com/supabase/supabase-grafana/refs/heads/main/grafana/dashboard.json). @@ -51,11 +69,11 @@ This dashboard includes 200+ charts grouped by CPU, IO, connections, replication height="1509" /> -## 4. Configure alerts (optional) +### 5. Configure alerts (optional) The [`docs/example-alerts.md`](https://github.com/supabase/supabase-grafana/blob/main/docs/example-alerts.md) file contains suggested alert rules (disk saturation, long-running queries, replication lag, etc.). Import the alert rules into Grafana Cloud’s Alerting UI or translate them into Grafana Cloud’s managed alert rule format. -## 5. Troubleshooting +### 6. Troubleshooting - Metrics missing? Ensure the Grafana Cloud agent can reach `https://.supabase.co` and that the selected Secret API key is still valid. - 401 errors? Create/rotate a Secret API key in [Project Settings → API Keys](/dashboard/project/_/settings/api-keys) and update the Grafana Cloud credentials. diff --git a/apps/docs/content/guides/telemetry/metrics/grafana-self-hosted.mdx b/apps/docs/content/guides/telemetry/metrics/grafana-self-hosted.mdx index 917ecfbc9e8..868e095ea6c 100644 --- a/apps/docs/content/guides/telemetry/metrics/grafana-self-hosted.mdx +++ b/apps/docs/content/guides/telemetry/metrics/grafana-self-hosted.mdx @@ -6,11 +6,11 @@ description: 'Deploy Prometheus and Grafana yourself to monitor Supabase metrics Self-hosting [Prometheus](https://prometheus.io/docs/prometheus/latest/installation/) and Grafana gives you full control over retention, alert routing, and dashboards. The Supabase Metrics API slots into any standard Prometheus scrape job, so you can run everything locally, on a VM, or inside Kubernetes. -<$Partial path="metrics_access.mdx" /> + - +Use this guide only if you need full manual control (custom scrape topology, self-hosted Prometheus or non-standard auth). -Grafana also documents a [Supabase integration reference](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/integrations/integration-reference/integration-supabase/). While it targets Grafana Cloud, the scrape and agent settings apply equally to self-hosted clusters and offer a community-maintained companion to this guide. +Otherwise, use the [Grafana Cloud integration](/docs/guides/telemetry/metrics/grafana-cloud#installation) available in the Supabase Dashboard. @@ -18,7 +18,7 @@ Grafana also documents a [Supabase integration reference](https://grafana.com/do 1. **Prometheus** scrapes `https://.supabase.co/customer/v1/privileged/metrics` every minute using HTTP Basic Auth. 2. **Grafana** reads from Prometheus and renders dashboards/alerts. -3. (Optional) **Alertmanager** or your preferred system sends notifications when Prometheus rules fire. +3. **Prometheus Alertmanager** or your preferred system sends notifications when Prometheus rules fire (optional) . ## 1. Deploy Prometheus From 3effea71aa7f205cdac3cbac034913683d40758d Mon Sep 17 00:00:00 2001 From: shaziya <99940835+shaziyabandukia@users.noreply.github.com> Date: Thu, 23 Jul 2026 06:56:48 -0700 Subject: [PATCH 016/141] feat(www): add Grafana Cloud partner drop blog post (#47716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Launch blog post - Grafana! ## What is the current behavior? Nonexistent 😆 ## What is the new behavior? Adds the "Observability for every Supabase project with Grafana Cloud" partner drop post, announcing the one-click Grafana Cloud integration. - Post: [`apps/www/_blog/2026-07-23-observability-for-every-supabase-project-with-grafana-cloud.mdx`](https://github.com/supabase/supabase/blob/blog/grafana-cloud-partner-drop/apps/www/_blog/2026-07-23-observability-for-every-supabase-project-with-grafana-cloud.mdx) - Images: `apps/www/public/images/blog/observability-for-every-supabase-project-with-grafana-cloud/` (og + thumb) - Author: `raminder_singh` · Date: 2026-07-23 · Category: product - Includes the YouTube walkthrough embed and UTM-tagged dashboard links ## Summary by CodeRabbit * **New Features** * Added a blog post announcing one-click Grafana Cloud observability integration for Supabase projects. * Highlights include preconfigured authentication, metric scraping, and dashboards available on all plans, including free. * Documents dashboard portability, Metrics API usage, setup instructions, and upcoming log support. --- ...ry-supabase-project-with-grafana-cloud.mdx | 60 ++++++++++++++++++ .../grafana-og.png | Bin 0 -> 313262 bytes .../grafana-thumb.png | Bin 0 -> 236862 bytes 3 files changed, 60 insertions(+) create mode 100644 apps/www/_blog/2026-07-23-observability-for-every-supabase-project-with-grafana-cloud.mdx create mode 100644 apps/www/public/images/blog/observability-for-every-supabase-project-with-grafana-cloud/grafana-og.png create mode 100644 apps/www/public/images/blog/observability-for-every-supabase-project-with-grafana-cloud/grafana-thumb.png diff --git a/apps/www/_blog/2026-07-23-observability-for-every-supabase-project-with-grafana-cloud.mdx b/apps/www/_blog/2026-07-23-observability-for-every-supabase-project-with-grafana-cloud.mdx new file mode 100644 index 00000000000..1ae2bfa11cc --- /dev/null +++ b/apps/www/_blog/2026-07-23-observability-for-every-supabase-project-with-grafana-cloud.mdx @@ -0,0 +1,60 @@ +--- +title: 'Observability for every Supabase project with Grafana Cloud' +description: 'Connect your Supabase project to Grafana Cloud in one click. A pre-built dashboard, alerting, and metrics: available on every plan, including free.' +author: raminder_singh +date: '2026-07-23' +categories: + - product +tags: + - integrations + - observability + - monitoring + - postgres +imgSocial: observability-for-every-supabase-project-with-grafana-cloud/grafana-og.png +imgThumb: observability-for-every-supabase-project-with-grafana-cloud/grafana-thumb.png +toc_depth: 2 +--- + +The [Grafana Cloud integration](https://supabase.com/dashboard/project/_/integrations/grafana-cloud/overview?utm_source=supabase-announcement&utm_medium=blog&utm_campaign=supabase_grafana_cloud_one_click_integration_launch) is available in Supabase. You've been able to connect Supabase to Grafana Cloud for years, and now you can connect to it instantly with the push of a button. + +From your Supabase Dashboard, in a single click, you get a fully-configured Grafana Cloud instance with authentication, metric scraping, and a pre-built observability dashboard already set up on any plan, including free. + +[Grafana Cloud](https://grafana.com/products/cloud/) is the AI-powered open observability cloud built on open source and open standards. Used by developers everywhere, the full-stack observability platform helps you improve the reliability of your applications, resolve incidents quickly, and optimize your telemetry to reduce noise and costs. + +
    +
    -The hosted Supabase Platform supports the [`pg_cron` extension](/docs/guides/database/extensions/pgcron), a recurring job scheduler in Postgres. +The hosted Supabase Platform supports the [`pg_cron` extension](/docs/guides/database/extensions/pg_cron), a recurring job scheduler in Postgres. -In combination with the [`pg_net` extension](/docs/guides/database/extensions/pgnet), this allows us to invoke Edge Functions periodically on a set schedule. +In combination with the [`pg_net` extension](/docs/guides/database/extensions/pg_net), this allows us to invoke Edge Functions periodically on a set schedule. @@ -57,5 +57,5 @@ select ## Resources -- [`pg_net` extension](/docs/guides/database/extensions/pgnet) -- [`pg_cron` extension](/docs/guides/database/extensions/pgcron) +- [`pg_net` extension](/docs/guides/database/extensions/pg_net) +- [`pg_cron` extension](/docs/guides/database/extensions/pg_cron) diff --git a/apps/docs/content/guides/getting-started/architecture.mdx b/apps/docs/content/guides/getting-started/architecture.mdx index 0e8767f5c3a..46899d686f2 100644 --- a/apps/docs/content/guides/getting-started/architecture.mdx +++ b/apps/docs/content/guides/getting-started/architecture.mdx @@ -52,7 +52,7 @@ An open source Dashboard for managing your database and services. A JWT-based API for managing users and issuing access tokens. This integrates with Postgres's Row Level Security and the API servers. -- Official Docs: [Supabase Auth reference docs](/docs/reference/auth) +- Official Docs: [Supabase Auth reference docs](/docs/reference/self-hosting-auth/start) - Source code: [github.com/supabase/gotrue](https://github.com/supabase/gotrue) - License: [MIT](https://github.com/supabase/gotrue/blob/master/LICENSE) - Language: Go @@ -80,7 +80,7 @@ A scalable WebSocket engine for managing user Presence, broadcasting messages, a An S3-compatible object storage service that stores metadata in Postgres. -- Official Docs: [Supabase Storage reference docs](/docs/reference/storage) +- Official Docs: [Supabase Storage reference docs](/docs/reference/self-hosting-storage/start) - Source code: [github.com/supabase/storage-api](https://github.com/supabase/storage-api) - License: [Apache 2.0](https://github.com/supabase/storage-api/blob/master/LICENSE) - Language: Node.js / TypeScript diff --git a/apps/docs/content/guides/getting-started/features.mdx b/apps/docs/content/guides/getting-started/features.mdx index c25c6cf7e89..2fd7bbbfe5e 100644 --- a/apps/docs/content/guides/getting-started/features.mdx +++ b/apps/docs/content/guides/getting-started/features.mdx @@ -10,7 +10,7 @@ This is a non-exhaustive list of features that Supabase provides for every proje ### Postgres database -Every project is a full Postgres database. [Docs](/docs/guides/database). +Every project is a full Postgres database. [Docs](/docs/guides/database/overview). ### Vector database @@ -56,11 +56,11 @@ Enforce Postgres clients to connect via SSL. [Docs](/docs/guides/platform/ssl-en ### Branching -Use Supabase Branches to test and preview changes. [Docs](/docs/guides/platform/branching). +Use Supabase Branches to test and preview changes. [Docs](/docs/guides/deployment/branching). ### Terraform provider -Manage Supabase infrastructure via Terraform, an Infrastructure as Code tool. [Docs](/docs/guides/platform/terraform). +Manage Supabase infrastructure via Terraform, an Infrastructure as Code tool. [Docs](/docs/guides/deployment/terraform). ### Read replicas @@ -68,7 +68,7 @@ Deploy read-only databases across multiple regions, for lower latency and better ### Log drains -Export Supabase logs to 3rd party providers and external tooling. [Docs](/docs/guides/platform/log-drains). +Export Supabase logs to third-party providers and external tooling. [Docs](/docs/guides/telemetry/log-drains). ## Studio @@ -96,7 +96,7 @@ Synchronize shared state across your users, including online status and typing i ### Email login -Build email logins for your application or website. [Docs](/docs/guides/auth/auth-email). +Build email logins for your application or website. [Docs](/docs/guides/auth/passwords). ### Social login @@ -108,7 +108,7 @@ Provide phone logins using a third-party SMS provider. [Docs](/docs/guides/auth/ ### Passwordless login -Build passwordless logins via magic links for your application or website. [Docs](/docs/guides/auth/auth-magic-link). +Build passwordless logins via magic links for your application or website. [Docs](/docs/guides/auth/auth-email-passwordless). ### Authorization via Row Level Security diff --git a/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx b/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx index e04e38c96b6..877c240120d 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx @@ -5,7 +5,7 @@ description: 'Learn how to use Supabase in your Android Kotlin App.' This tutorial demonstrates how to build a basic product management app. The app demonstrates management operations, photo upload, account creation and authentication using: -- [Supabase Database](/docs/guides/database) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information. +- [Supabase Database](/docs/guides/database/overview) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information. - [Supabase Auth](/docs/guides/auth) - users log in through magic links sent to their email (without having to set up a password). - [Supabase Storage](/docs/guides/storage) - users can upload a profile photo. diff --git a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration.mdx b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration.mdx index fae9d946305..dbed6d8faf1 100644 --- a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration.mdx +++ b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration.mdx @@ -35,7 +35,7 @@ Within your app's UI, redirect the user to [`https://api.supabase.com/v1/oauth/a - `state`: Information about the state of your app. Note that `redirect_uri` and `state` together cannot exceed 4kB in size. - `organization_slug`: The slug of the organization you want to connect to. This is optional, but if provided, it will pre-select the organization for the user. - [Recommended] PKCE: We strongly recommend using the PKCE flow for increased security. Generate a random value before taking the user to the authorize endpoint. This value is called code verifier. Hash it with SHA256 and include it as the `code_challenge` parameter, while setting `code_challenge_method` to `S256`. In the next step, you would need to provide the code verifier to get the first access and refresh token. -- [Deprecated] `scope`: Scopes are configured when you create your OAuth app. Read the [docs](/docs/guides/platform/oauth-apps/oauth-scopes) for more details. +- [Deprecated] `scope`: Scopes are configured when you create your OAuth app. Read the [docs](/docs/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes) for more details. ```ts router.get('/connect-supabase/login', async (ctx) => { diff --git a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx index 2e749fc4da2..d40563b3f4a 100644 --- a/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx +++ b/apps/docs/content/guides/integrations/build-a-supabase-oauth-integration/oauth-scopes.mdx @@ -7,15 +7,15 @@ subtitle: 'Scopes let you specify the level of access your integration needs' -Scopes are only available for OAuth apps. Check out [**our guide**](/docs/guides/platform/oauth-apps/build-a-supabase-integration) to learn how to build an OAuth app integration. +Scopes are only available for OAuth apps. Check out [**our guide**](/docs/guides/integrations/build-a-supabase-oauth-integration) to learn how to build an OAuth app integration. Scopes restrict access to the specific [Supabase Management API endpoints](/docs/reference/api/introduction) for OAuth tokens. All scopes can be specified as read and/or write. -Scopes are set when you [create an OAuth app](/docs/guides/platform/oauth-apps/build-a-supabase-integration#create-an-oauth-app) in the Supabase Dashboard. +Scopes are set when you [create an OAuth app](/docs/guides/integrations/build-a-supabase-oauth-integration#create-an-oauth-app) in the Supabase Dashboard. -You can update scopes of your OAuth app at any time, but existing OAuth app users will need to re-authorize your app via the [OAuth flow](/docs/guides/integrations/build-a-supabase-integration#implementing-the-oauth-20-flow) to apply the new scopes. +You can update scopes of your OAuth app at any time, but existing OAuth app users will need to re-authorize your app via the [OAuth flow](/docs/guides/integrations/build-a-supabase-oauth-integration#implementing-the-oauth-20-flow) to apply the new scopes. ## Available scopes diff --git a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx index 45be844a97f..f20fca78d71 100644 --- a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx +++ b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx @@ -13,7 +13,7 @@ This document will guide you on best practices when using Supabase for your own ## Overview -All features of Supabase can be managed through the [Management API](/docs/reference/api/introduction) or the [remote MCP Server](/docs/guides/getting-started/mcp). +All features of Supabase can be managed through the [Management API](/docs/reference/api/introduction) or the [remote MCP Server](/docs/guides/ai-tools/mcp). ## Launching projects diff --git a/apps/docs/content/guides/local-development/cli/getting-started.mdx b/apps/docs/content/guides/local-development/cli/getting-started.mdx index 5ea553217f6..18e38612983 100644 --- a/apps/docs/content/guides/local-development/cli/getting-started.mdx +++ b/apps/docs/content/guides/local-development/cli/getting-started.mdx @@ -384,7 +384,7 @@ Local logs rely on the Supabase Analytics Server which accesses the docker loggi -For advanced logs analysis using the Logs Explorer, it is advised to use the BigQuery backend instead of the default Postgres backend. Read about the steps [here](/docs/reference/self-hosting-analytics/introduction#bigquery). +For advanced logs analysis using the Logs Explorer, it is advised to use the BigQuery backend instead of the default Postgres backend. Read about the steps [here](/docs/reference/self-hosting-analytics/introduction#using-the-bigquery-backend). diff --git a/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx b/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx index 2fc83fe1ee9..d319bdf75a2 100644 --- a/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx +++ b/apps/docs/content/guides/local-development/cli/testing-and-linting.mdx @@ -44,7 +44,7 @@ By default, Mailpit is available at [localhost:54324](http://localhost:54324) wh ### Going into production -The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/platform/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, configure your own email provider by enabling SMTP credentials in your [project settings](/dashboard/project/_/auth/smtp). +The "default" email provided by Supabase is only for development purposes. It is [heavily restricted](/docs/guides/deployment/going-into-prod#auth-rate-limits) to ensure that it is not used for spam. Before going into production, configure your own email provider by enabling SMTP credentials in your [project settings](/dashboard/project/_/auth/smtp). ## Linting your database diff --git a/apps/docs/content/guides/local-development/managing-config.mdx b/apps/docs/content/guides/local-development/managing-config.mdx index 018aba75e3f..623cb3316c1 100644 --- a/apps/docs/content/guides/local-development/managing-config.mdx +++ b/apps/docs/content/guides/local-development/managing-config.mdx @@ -10,7 +10,7 @@ The Supabase CLI uses a `config.toml` file to manage local configuration. This f The `config.toml` file is automatically created when you run `supabase init`. -There are a wide variety of options available, which can be found in the [CLI Config Reference](/docs/guides/cli/config). +There are a wide variety of options available, which can be found in the [CLI Config Reference](/docs/guides/local-development/cli/config). For example, to enable the "Apple" OAuth provider for local development, you can append the following information to `config.toml`: diff --git a/apps/docs/content/guides/platform.mdx b/apps/docs/content/guides/platform.mdx index 3953d8a1862..7d99b54413c 100644 --- a/apps/docs/content/guides/platform.mdx +++ b/apps/docs/content/guides/platform.mdx @@ -13,8 +13,8 @@ Visit [supabase.com/dashboard](/dashboard) and sign in to start creating project Each project on Supabase comes with: -- A dedicated [Postgres database](/docs/guides/database) -- [Auto-generated APIs](/docs/guides/database/api) +- A dedicated [Postgres database](/docs/guides/database/overview) +- [Auto-generated APIs](/docs/guides/api) - [Auth and user management](/docs/guides/auth) - [Edge Functions](/docs/guides/functions) - [Realtime API](/docs/guides/realtime) diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 5c4941ac7f4..c4c7567395d 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -12,7 +12,7 @@ subtitle: 'This documentation covers frequently asked questions around subscript #### What are organizations and projects? The Supabase Platform has "organizations" and "projects". An organization may contain multiple projects. Each project is a dedicated Supabase instance with all of its sub-services including Storage, Auth, Functions and Realtime. -Each organization only has a single subscription with a single plan (Free, Pro, Team or Enterprise). Project add-ons such as [Compute](/docs/guides/platform/compute-add-ons), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/platform/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone), [Custom Domains](/docs/guides/platform/custom-domains) and [PITR](/docs/guides/platform/backups#point-in-time-recovery) are configured per project and are added to your organization subscription. +Each organization only has a single subscription with a single plan (Free, Pro, Team or Enterprise). Project add-ons such as [Compute](/docs/guides/platform/compute-and-disk), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/telemetry/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone), [Custom Domains](/docs/guides/platform/custom-domains) and [PITR](/docs/guides/platform/backups#point-in-time-recovery) are configured per project and are added to your organization subscription. Read more on [About billing on Supabase](/docs/guides/platform/billing-on-supabase#organization-based-billing). @@ -54,7 +54,7 @@ Refer to our [Compute](/docs/guides/platform/manage-your-usage/compute#billing-e #### How does compute billing work? -Each Supabase project is a dedicated VM and Postgres database. By default, your instance runs on the Micro compute instance. You have the option to upgrade your compute size in your [Project settings](/dashboard/project/_/settings/addons). See [Compute Add-ons](/docs/guides/platform/compute-add-ons) for available options. +Each Supabase project is a dedicated VM and Postgres database. By default, your instance runs on the Micro compute instance. You have the option to upgrade your compute size in your [Project settings](/dashboard/project/_/settings/addons). See [Compute Add-ons](/docs/guides/platform/compute-and-disk) for available options. When you change your compute size, there are no immediate upfront charges. Instead, you will be billed based on the compute hours during your billing cycle reset. @@ -132,7 +132,7 @@ The Fair Use Policy is applied through service restrictions. This could mean: - Pausing projects - Switching databases to read-only mode - Disabling new project launches/transfers -- Responding with a [402 status code](/docs/guides/platform/http-status-codes#402-service-restriction) for all API requests +- Responding with a [402 status code](/docs/guides/troubleshooting/http-status-codes#402-service-restriction) for all API requests The Fair Use Policy is generally applied to all projects of the restricted organization. diff --git a/apps/docs/content/guides/platform/custom-domains.mdx b/apps/docs/content/guides/platform/custom-domains.mdx index d0076126fa8..c294cbfbbb6 100644 --- a/apps/docs/content/guides/platform/custom-domains.mdx +++ b/apps/docs/content/guides/platform/custom-domains.mdx @@ -40,8 +40,8 @@ This example assumes your Supabase project is `abcdefghijklmnopqrst` with a corr To get started: -1. [Install](/docs/guides/resources/supabase-cli) the latest version of the Supabase CLI. -2. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI. +1. [Install](/docs/guides/local-development) the latest version of the Supabase CLI. +2. [Log in](/docs/guides/local-development/database-migrations#log-in-to-the-supabase-cli) to your Supabase account using the CLI. 3. Ensure you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. 4. Get a custom domain from a DNS provider. Currently, only subdomains are supported. - Use `api.example.com` instead of `example.com`. @@ -152,8 +152,8 @@ Vanity subdomains allow you to present a basic branded experience, compared to c To get started: -1. [Install](/docs/guides/resources/supabase-cli) the latest version of the Supabase CLI. -1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI. +1. [Install](/docs/guides/local-development) the latest version of the Supabase CLI. +1. [Log in](/docs/guides/local-development/database-migrations#log-in-to-the-supabase-cli) to your Supabase account using the CLI. 1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project you'd like to set up a vanity subdomain for. 1. Ensure that your organization is on a paid plan (Pro/Team/Enterprise Plan) in the [Billing page of the Dashboard](/dashboard/org/_/billing). diff --git a/apps/docs/content/guides/platform/hipaa-projects.mdx b/apps/docs/content/guides/platform/hipaa-projects.mdx index 78f1864bce1..6ac8129029d 100644 --- a/apps/docs/content/guides/platform/hipaa-projects.mdx +++ b/apps/docs/content/guides/platform/hipaa-projects.mdx @@ -21,7 +21,7 @@ The required project configuration is outlined in the [shared responsibility mod These include: -- Enabling [Point in Time Recovery](/docs/guides/platform/backups#point-in-time-recovery) which requires at least a [small compute add-on](/docs/guides/platform/compute-add-ons). +- Enabling [Point in Time Recovery](/docs/guides/platform/backups#point-in-time-recovery) which requires at least a [small compute add-on](/docs/guides/platform/compute-and-disk). - Turning on [SSL Enforcement](/docs/guides/platform/ssl-enforcement). - Enabling [Network Restrictions](/docs/guides/platform/network-restrictions). - Keeping [Postgres connection logging](/docs/guides/platform/postgres-connection-logging) enabled. diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx index 5323e90b9f9..ad4399f9e2c 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-auth.mdx @@ -83,8 +83,8 @@ For more advanced migrations, including the use of a middleware server component ## Resources - [Supabase vs Firebase](/alternatives/supabase-vs-firebase) -- [Firestore Data Migration](/docs/guides/migrations/firestore-data) -- [Firestore Storage Migration](/docs/guides/migrations/firebase-storage) +- [Firestore Data Migration](/docs/guides/platform/migrating-to-supabase/firestore-data) +- [Firestore Storage Migration](/docs/guides/platform/migrating-to-supabase/firebase-storage) ## Migrate to Supabase diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx index 1da520b1838..12a84f98cad 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firebase-storage.mdx @@ -62,8 +62,8 @@ If the bucket doesn't exist, it's created as a `non-public` bucket. You must set ## Resources - [Supabase vs Firebase](/alternatives/supabase-vs-firebase) -- [Firestore Data Migration](/docs/guides/migrations/firestore-data) -- [Firebase Auth Migration](/docs/guides/migrations/firebase-auth) +- [Firestore Data Migration](/docs/guides/platform/migrating-to-supabase/firestore-data) +- [Firebase Auth Migration](/docs/guides/platform/migrating-to-supabase/firebase-auth) ## Migrate to Supabase diff --git a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx index aafd172a0b4..647a5c880cb 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase/firestore-data.mdx @@ -206,8 +206,8 @@ The result is two separate JSON files: ## Resources - [Supabase vs Firebase](/alternatives/supabase-vs-firebase) -- [Firestore Storage Migration](/docs/guides/migrations/firebase-storage) -- [Firebase Auth Migration](/docs/guides/migrations/firebase-auth) +- [Firestore Storage Migration](/docs/guides/platform/migrating-to-supabase/firebase-storage) +- [Firebase Auth Migration](/docs/guides/platform/migrating-to-supabase/firebase-auth) ## Migrate to Supabase diff --git a/apps/docs/content/guides/platform/network-restrictions.mdx b/apps/docs/content/guides/platform/network-restrictions.mdx index b91a0ddc290..b471ba27eab 100644 --- a/apps/docs/content/guides/platform/network-restrictions.mdx +++ b/apps/docs/content/guides/platform/network-restrictions.mdx @@ -27,9 +27,9 @@ To configure network restrictions with the dashboard: To configure network restrictions with the CLI: -1. [Install](/docs/guides/cli) the Supabase CLI 1.22.0+. -1. [Log in](/docs/guides/cli/local-development#log-in-to-the-supabase-cli) to your Supabase account. -1. If your project was created before December 23, 2022, [upgrade it to the latest Supabase version](/docs/guides/platform/migrating-and-upgrading-projects) before using network restrictions. +1. [Install](/docs/guides/local-development) the Supabase CLI 1.22.0+. +1. [Log in](/docs/guides/local-development/database-migrations#log-in-to-the-supabase-cli) to your Supabase account. +1. If your project was created before December 23, 2022, [upgrade it to the latest Supabase version](/docs/guides/platform/upgrading) before using network restrictions. 1. Ensure you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. ### Check restrictions @@ -101,5 +101,5 @@ To remove all network restrictions: ## Limitations -- Network restrictions apply to Postgres and the database pooler. They don't apply to HTTPS APIs such as PostgREST, Storage, and Auth, or to Supabase client libraries like [supabase-js](/docs/reference/javascript). -- With network restrictions applied, Edge functions lose direct access to the database. Use [supabase-js](/docs/reference/javascript) to connect to the database from Edge Functions instead. +- Network restrictions apply to Postgres and the database pooler. They don't apply to HTTPS APIs such as PostgREST, Storage, and Auth, or to Supabase client libraries like [supabase-js](/docs/reference/javascript/introduction). +- With network restrictions applied, Edge functions lose direct access to the database. Use [supabase-js](/docs/reference/javascript/introduction) to connect to the database from Edge Functions instead. diff --git a/apps/docs/content/guides/platform/performance.mdx b/apps/docs/content/guides/platform/performance.mdx index ae0005551b2..c93edb08b0e 100644 --- a/apps/docs/content/guides/platform/performance.mdx +++ b/apps/docs/content/guides/platform/performance.mdx @@ -31,7 +31,7 @@ In such a scenario, you can consider: You can use the [pg_stat_activity](https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW) view to debug which clients are holding open connections on your DB. `pg_stat_activity` only exposes information on direct connections to the database. Information on the number of connections to Supavisor is available [via the metrics endpoint](../telemetry/metrics). -Depending on the clients involved, you might be able to configure them to work with fewer connections (e.g. by imposing a limit on the maximum number of connections they're allowed to use), or shift specific workloads to connect via [Supavisor](/docs/guides/database/connecting-to-postgres#connection-pooler) instead. Transient workflows, which can scale up and down rapidly in response to traffic (e.g. serverless functions), can especially benefit from using a connection pooler rather than connecting to the DB directly. +Depending on the clients involved, you might be able to configure them to work with fewer connections (e.g. by imposing a limit on the maximum number of connections they're allowed to use), or shift specific workloads to connect via [Supavisor](/docs/guides/database/connecting-to-postgres#poolers) instead. Transient workflows, which can scale up and down rapidly in response to traffic (e.g. serverless functions), can especially benefit from using a connection pooler rather than connecting to the DB directly. ### Allowing higher number of connections diff --git a/apps/docs/content/guides/platform/project-transfer.mdx b/apps/docs/content/guides/platform/project-transfer.mdx index 5ba93e504f9..e463bf2aa77 100644 --- a/apps/docs/content/guides/platform/project-transfer.mdx +++ b/apps/docs/content/guides/platform/project-transfer.mdx @@ -43,7 +43,7 @@ Target organization - the organization you want to move the project to ## Usage-billing and project add-ons -For usage metrics such as disk size, egress or image transformations and project add-ons such as [Compute Add-On](/docs/guides/platform/compute-add-ons), [Point-In-Time-Recovery](/docs/guides/platform/backups#point-in-time-recovery), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/platform/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone) or a [Custom Domain](/docs/guides/platform/custom-domains), the source organization will still be charged for the usage up until the transfer. The charges will be added to the invoice when the billing cycle resets. +For usage metrics such as disk size, egress or image transformations and project add-ons such as [Compute Add-On](/docs/guides/platform/compute-and-disk), [Point-In-Time-Recovery](/docs/guides/platform/backups#point-in-time-recovery), [IPv4](/docs/guides/platform/ipv4-address), [Log Drains](/docs/guides/telemetry/log-drains), [Advanced MFA](/docs/guides/auth/auth-mfa/phone) or a [Custom Domain](/docs/guides/platform/custom-domains), the source organization will still be charged for the usage up until the transfer. The charges will be added to the invoice when the billing cycle resets. The target organization will be charged at the end of the billing cycle for usage after the project transfer. diff --git a/apps/docs/content/guides/platform/read-replicas/getting-started.mdx b/apps/docs/content/guides/platform/read-replicas/getting-started.mdx index bfb7cf827e9..0d895570ee1 100644 --- a/apps/docs/content/guides/platform/read-replicas/getting-started.mdx +++ b/apps/docs/content/guides/platform/read-replicas/getting-started.mdx @@ -15,11 +15,11 @@ Read Replicas are available for all projects on the Pro, Team and Enterprise pla Projects must meet these requirements to use Read Replicas: 1. Running on AWS. -2. Running on at least a [Small compute add-on](/docs/guides/platform/compute-add-ons). +2. Running on at least a [Small compute add-on](/docs/guides/platform/compute-and-disk). - Read Replicas are started on the same compute instance as the Primary to keep up with changes. 3. Running on Postgres 15+. - - For projects running on older versions of Postgres, you need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade). + - For projects running on older versions of Postgres, you need to [upgrade to the latest platform version](/docs/guides/platform/upgrading). 4. Not using [legacy logical backups](/docs/guides/platform/backups#point-in-time-recovery) - Physical backups are automatically enabled if using [Point in time recovery (PITR)](/docs/guides/platform/backups#point-in-time-recovery) @@ -110,7 +110,7 @@ Once the Primary database has completed restarting (or resizing, in case of a co The following procedures require all Read Replicas for a project to be brought down before performing them: -1. [Project upgrades](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade) +1. [Project upgrades](/docs/guides/platform/upgrading) 2. [Data restorations](/docs/guides/platform/backups#pitr-restoration-process) These operations need to complete before you can re-deploy Read Replicas. diff --git a/apps/docs/content/guides/platform/ssl-enforcement.mdx b/apps/docs/content/guides/platform/ssl-enforcement.mdx index 2f70acddc3f..726596f60bc 100644 --- a/apps/docs/content/guides/platform/ssl-enforcement.mdx +++ b/apps/docs/content/guides/platform/ssl-enforcement.mdx @@ -61,8 +61,8 @@ curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/ssl-enforcement" To get started: -1. [Install](/docs/guides/cli) the Supabase CLI 1.37.0+. -1. [Log in](/docs/guides/getting-started/local-development#log-in-to-the-supabase-cli) to your Supabase account using the CLI. +1. [Install](/docs/guides/local-development) the Supabase CLI 1.37.0+. +1. [Log in](/docs/guides/local-development/database-migrations#log-in-to-the-supabase-cli) to your Supabase account using the CLI. 1. Ensure that you have [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project that you are enabling SSL enforcement. ### Check enforcement status diff --git a/apps/docs/content/guides/realtime/reports.mdx b/apps/docs/content/guides/realtime/reports.mdx index 427f8fae047..fe6bd5697a4 100644 --- a/apps/docs/content/guides/realtime/reports.mdx +++ b/apps/docs/content/guides/realtime/reports.mdx @@ -61,7 +61,7 @@ height={625} | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Configure connection limit | Adjust the "Max concurrent connections" setting to increase or decrease the connection limit for your project | [Realtime Settings Guide](/docs/guides/realtime/settings) | | Upgrade plan | Increase available client connections. Connection limits vary by plan: Free (200), Pro (500), Pro no spend cap (10,000), Team (10,000), Enterprise (10,000+) | [Pricing and Plans](/pricing) | -| Review quotas | Understand connection limits and other Realtime quotas for your plan | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Review quotas | Understand connection limits and other Realtime quotas for your plan | [Realtime Quotas Reference](/docs/guides/realtime/limits) | | Understand connection quota | Learn how the concurrent connections quota works and how to configure it for your plan | [Concurrent Peak Connections Quota Troubleshooting](/docs/guides/troubleshooting/realtime-concurrent-peak-connections-quota-jdDqcp) | | Fix silent disconnections | Fix connection issues in background applications using heartbeat callbacks and Web Workers | [Handling Silent Disconnections in Background Apps](/docs/guides/troubleshooting/realtime-handling-silent-disconnections-in-backgrounded-applications-592794) | | Check logs | Investigate connection errors and quota errors in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | @@ -90,7 +90,7 @@ height={625} | Action | Description | More information | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Configure event limits | Adjust "Max events per second" and "Max payload size in KB" settings to optimize broadcast throughput and message size limits | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Review quotas | Understand message per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and broadcast payload size limits (Free: 256 KB, Pro+: 3,000 KB) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Review quotas | Understand message per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and broadcast payload size limits (Free: 256 KB, Pro+: 3,000 KB) | [Realtime Quotas Reference](/docs/guides/realtime/limits) | | Check logs | Investigate broadcast errors or quota limit issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | | Debug with logger | Enable logging to track messages sent and received, and diagnose broadcast delivery issues | [Debugging Realtime with Logger](/docs/guides/troubleshooting/realtime-debugging-with-logger) | | Learn broadcast basics | Understand how to implement and optimize broadcast messaging in your application | [Broadcast Guide](/docs/guides/realtime/broadcast) | @@ -119,7 +119,7 @@ height={625} | Action | Description | More information | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Configure presence limits | Adjust the "Max presence events per second" setting to optimize presence state update throughput | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Review quotas | Understand presence messages per second limits (Free: 20, Pro: 50, Pro no spend cap/Team/Enterprise: 1,000) and presence keys per object limits (10 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Review quotas | Understand presence messages per second limits (Free: 20, Pro: 50, Pro no spend cap/Team/Enterprise: 1,000) and presence keys per object limits (10 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/limits) | | Check logs | Investigate presence errors or quota limit issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | | Debug with logger | Enable logging to track presence events and diagnose state synchronization issues | [Debugging Realtime with Logger](/docs/guides/troubleshooting/realtime-debugging-with-logger) | | Learn presence basics | Understand how to implement and optimize presence state tracking in your application | [Presence Guide](/docs/guides/realtime/presence) | @@ -147,7 +147,7 @@ height={625} | Action | Description | More information | | ------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Review quotas | Understand Postgres change payload size limits (1,024 KB for all plans) and message throughput limits | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Review quotas | Understand Postgres change payload size limits (1,024 KB for all plans) and message throughput limits | [Realtime Quotas Reference](/docs/guides/realtime/limits) | | Check logs | Investigate Postgres Changes errors or performance issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | | Learn Postgres Changes | Understand limitations and best practices for using Postgres Changes | [Postgres Changes Guide](/docs/guides/realtime/postgres-changes) | | Migrate to Broadcast | For better scalability, consider using Broadcast with database triggers instead of Postgres Changes | [Broadcast Guide](/docs/guides/realtime/broadcast) | @@ -177,7 +177,7 @@ height={625} | Action | Description | More information | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -| Review quotas | Understand channel joins per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and channels per connection limits (100 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Review quotas | Understand channel joins per second limits (Free: 100, Pro: 500, Pro no spend cap/Team/Enterprise: 2,500) and channels per connection limits (100 for most plans) | [Realtime Quotas Reference](/docs/guides/realtime/limits) | | Check logs | Investigate `too_many_joins` errors or channel join failures in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | | Fix channel errors | Learn how to properly manage channel lifecycle and prevent channel leaks in your application | [TooManyChannels Error Troubleshooting](/docs/guides/troubleshooting/realtime-too-many-channels-error) | | Learn channel basics | Understand how Realtime channels work and best practices for channel management | [Realtime Channels Concepts](/docs/guides/realtime/concepts#channels) | @@ -206,7 +206,7 @@ height={625} | Action | Description | More information | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Configure payload limits | Adjust the "Max payload size in KB" setting to increase or decrease the maximum message size allowed | [Realtime Settings Guide](/docs/guides/realtime/settings) | -| Review quotas | Understand payload size limits: Broadcast (Free: 256 KB, Pro+: 3,000 KB) and Postgres Changes (1,024 KB for all plans) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Review quotas | Understand payload size limits: Broadcast (Free: 256 KB, Pro+: 3,000 KB) and Postgres Changes (1,024 KB for all plans) | [Realtime Quotas Reference](/docs/guides/realtime/limits) | | Check logs | Investigate payload-related errors or performance issues in your project dashboard | [Realtime Logs Dashboard](/dashboard/project/_/database/realtime-logs) | | Review benchmarks | Understand how payload size affects latency and throughput (larger payloads increase latency) | [Payload Size Performance Benchmarks](/docs/guides/realtime/benchmarks#broadcast-impact-of-payload-size) | | Debug query performance | Use `explain()` to analyze queries and identify performance bottlenecks that may be causing large payloads | [Query Performance Debugging Guide](/docs/guides/database/debugging-performance) | @@ -369,7 +369,7 @@ height={645} | Learn HTTP status codes | Learn about HTTP status codes including 4XX client errors and 5XX server errors | [HTTP Status Codes Troubleshooting](/docs/guides/troubleshooting/http-status-codes) | | Fix timeout errors | Resolve WebSocket timeout errors caused by Node.js version incompatibility | [TIMED_OUT Connection Errors Troubleshooting](/docs/guides/troubleshooting/realtime-connections-timed_out-status) | | Understand heartbeats | Monitor heartbeat status to detect connection issues and handle timeouts | [Realtime Heartbeats Guide](/docs/guides/troubleshooting/realtime-heartbeat-messages) | -| Review quotas | Check if errors are related to quota limits (e.g., `too_many_connections`, `too_many_joins`) | [Realtime Quotas Reference](/docs/guides/realtime/quotas) | +| Review quotas | Check if errors are related to quota limits (e.g., `too_many_connections`, `too_many_joins`) | [Realtime Quotas Reference](/docs/guides/realtime/limits) | | Learn authorization | Troubleshoot authorization-related errors for private channels | [Realtime Authorization Guide](/docs/guides/realtime/authorization) | | Contact support | Get assistance with persistent errors or investigate service-level issues | [Support Portal](/dashboard/support/new) | diff --git a/apps/docs/content/guides/telemetry/debugging.mdx b/apps/docs/content/guides/telemetry/debugging.mdx index bdce77234b6..1711e999833 100644 --- a/apps/docs/content/guides/telemetry/debugging.mdx +++ b/apps/docs/content/guides/telemetry/debugging.mdx @@ -73,6 +73,6 @@ Supabase updates these troubleshooting guides continuously, so treat this table | Webhook not firing; `pg_cron` job not running; `pg_net` queue stuck; `42501 ... http_request_queue` | Database jobs → `postgres_logs` | [Webhook debugging](/docs/guides/troubleshooting/webhook-debugging-guide-M8sk47) · [pg_cron debugging](/docs/guides/troubleshooting/pgcron-debugging-guide-n1KTaz) · [42501 http_request_queue](/docs/guides/troubleshooting/42501--permission-denied-for-table-httprequestqueue-KnozmQ) | | Reading or querying logs; interpreting Postgres logs; finding API errors in logs; reading metrics | Diagnostics → any log source | [Logging guide](/docs/guides/telemetry/logs) · [Interpret Postgres logs](/docs/guides/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj) · [API errors in logs](/docs/guides/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9) · [Logging levels](/docs/guides/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm) · [View database metrics](/docs/guides/troubleshooting/how-to-view-database-metrics-uqf2z_) | -For query performance and schema-design questions such as indexing, `EXPLAIN`, N+1 queries, or partitioning, see the [Postgres guides](/docs/guides/database). +For query performance and schema-design questions such as indexing, `EXPLAIN`, N+1 queries, or partitioning, see the [Postgres guides](/docs/guides/database/overview). Debugging is complete only once you've re-run the failing operation, confirmed it succeeds, and checked that the layer's logs show a clean result. diff --git a/apps/docs/content/guides/telemetry/logs.mdx b/apps/docs/content/guides/telemetry/logs.mdx index 16788ab950b..9b945fdf217 100644 --- a/apps/docs/content/guides/telemetry/logs.mdx +++ b/apps/docs/content/guides/telemetry/logs.mdx @@ -306,7 +306,7 @@ order by n desc limit 100; ``` -If you use the [Supabase MCP server](/docs/guides/getting-started/mcp), the `query_logs` tool runs a custom ClickHouse query like the ones above on hosted projects. The `get_logs` tool returns a service's recent logs without SQL; it is deprecated on hosted projects in favor of `query_logs`, and remains the option for local and self-hosted projects. +If you use the [Supabase MCP server](/docs/guides/ai-tools/mcp), the `query_logs` tool runs a custom ClickHouse query like the ones above on hosted projects. The `get_logs` tool returns a service's recent logs without SQL; it is deprecated on hosted projects in favor of `query_logs`, and remains the option for local and self-hosted projects. ### LIMIT and result row limitations diff --git a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx index bb89ba7c62e..c97a0574590 100644 --- a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx +++ b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx @@ -36,7 +36,7 @@ While pointing out the exact cause for egress may not be straightforward, there - Reduce the number of queries/calls by optimising client code or use caches to reduce the number of requests/queries being done: https://github.com/psteinroe/supabase-cache-helpers/ - In case of update/insert queries, if you don't need the entire row to be returned, configure your ORM/queries to not return the entire row - In case of running manual backups through Supavisor, remove unneeded tables and/or reduce the frequency -- For Storage, if you start using the [Smart CDN](/docs/guides/storage/cdn/smart-cdn) Storage Egress usage can be reduced. You can also use the [Supabase Image Transformations](/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. +- For Storage, if you start using the [Smart CDN](/docs/guides/storage/cdn/smart-cdn) Storage Egress usage can be reduced. You can also use the [Supabase Image Transformations](/docs/guides/storage/serving/image-transformations) to optimize the images and reduce the egress. **Cached vs uncached egress** diff --git a/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx b/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx index 5986805ee2c..2790aaf7035 100644 --- a/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx +++ b/apps/docs/content/troubleshooting/canceling-statement-due-to-statement-timeout-581wFv.mdx @@ -11,7 +11,7 @@ database_id = "4a41e734-6883-452f-bc8e-82ff4ac22a81" You can run this query to check the current settings set for your roles: `SELECT rolname, rolconfig FROM   pg_roles;` -To increase the `statement_timeout` for a specific role, you may follow the instructions [here](/docs/guides/database/timeouts#changing-the-default-timeout). Note that it may require a quick reboot for the changes to take effect. +To increase the `statement_timeout` for a specific role, you may follow the instructions [here](/docs/guides/database/postgres/timeouts#role-level). Note that it may require a quick reboot for the changes to take effect. Additionally, to check how long a query is taking, you can check the Query Performance report which can give you more information on the query's performance: https://app.supabase.com/project/_/advisors/query-performance. You can use the [query plan analyzer](https://www.postgresql.org/docs/current/sql-explain.html) on any expensive queries that you have identified: `explain analyze ;`. For supabase-js/ PostgREST queries you can use `.explain()`. diff --git a/apps/docs/content/troubleshooting/database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb.mdx b/apps/docs/content/troubleshooting/database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb.mdx index 8e50b248103..6a200efb796 100644 --- a/apps/docs/content/troubleshooting/database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb.mdx +++ b/apps/docs/content/troubleshooting/database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb.mdx @@ -17,4 +17,4 @@ To overcome this, the connections need to be optimized as mentioned here: https: Additionally, you can try using the connection pool to help solve this issue: https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler -If you're already using connection pooling and still hitting the maximum connections, then it is suggested to upgrade your compute add-on that allows more connections: https://supabase.com/docs/guides/platform/compute-add-ons +If you're already using connection pooling and still hitting the maximum connections, then it is suggested to upgrade your compute add-on that allows more connections: https://supabase.com/docs/guides/platform/compute-and-disk diff --git a/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx b/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx index 3d2571b9e93..b9caf4f6c0f 100644 --- a/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx +++ b/apps/docs/content/troubleshooting/grafana-not-displaying-data-sXJrMj.mdx @@ -7,7 +7,7 @@ keywords = [ "grafana", "docker", "metrics", "configuration" ] database_id = "76a4099e-450f-4b5b-a539-224760348c18" --- -This guide is for identifying configuration mistakes in [self-hosted Supabase Grafana installations](/docs/guides/monitoring-troubleshooting/metrics#deploying-supabase-grafana) +This guide is for identifying configuration mistakes in [self-hosted Supabase Grafana installations](/docs/guides/telemetry/metrics/grafana-self-hosted) ## Step 1: Ping your Grafana endpoint diff --git a/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx b/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx index 4682d7ed2dd..34b4650368b 100644 --- a/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx +++ b/apps/docs/content/troubleshooting/how-to-change-max-database-connections-_BQ8P5.mdx @@ -14,7 +14,7 @@ cli = [ "supabase-postgres-config-update" ] ## Changing max database connections -Each compute instance has a default direct connection and pooler connection settings. You can find the most recent settings in the [compute docs](/docs/guides/platform/compute-add-ons#disk-io): +Each compute instance has a default direct connection and pooler connection settings. You can find the most recent settings in the [compute docs](/docs/guides/platform/compute-and-disk#disk): | Compute Size | Direct Connections | Pooler Connections | | ------------ | ------------------ | ------------------ | @@ -34,7 +34,7 @@ Each compute instance has a default direct connection and pooler connection sett > Note: the Supavisor connection limits are hard-coded and cannot be changed without upgrading the compute size: -You can configure the maximum amount of connections that Postgres will tolerate with the [Supabase CLI.](/docs/guides/platform/custom-postgres-config) +You can configure the maximum amount of connections that Postgres will tolerate with the [Supabase CLI.](/docs/guides/database/custom-postgres-config) You can run the following commands: @@ -83,7 +83,7 @@ To avoid displacing cache or straining system resources, it is advised to not in Postgres will allow you to overcommit memory. You can run the below query to find out the hypothetical max value you could change it to without risking memory failure: -> NOTE: You can find your server memory in the [compute add-ons docs](/docs/guides/platform/compute-add-ons) +> NOTE: You can find your server memory in the [compute add-ons docs](/docs/guides/platform/compute-and-disk) ```sql select diff --git a/apps/docs/content/troubleshooting/http-status-codes.mdx b/apps/docs/content/troubleshooting/http-status-codes.mdx index 25c3346858c..3a0cade2293 100644 --- a/apps/docs/content/troubleshooting/http-status-codes.mdx +++ b/apps/docs/content/troubleshooting/http-status-codes.mdx @@ -50,7 +50,7 @@ See the [Fair Use Policy](/docs/guides/platform/billing-faq#fair-use-policy) sec 5XX status codes indicate that the project was unable to process the request successfully, but not because of an issue with the client's request. -5XX status codes can be the result of the project not having enough [compute](/docs/guides/platform/compute-add-ons) to process a complex request being made by a client or not being able to keep up with the volume of requests made against the project. +5XX status codes can be the result of the project not having enough [compute](/docs/guides/platform/compute-and-disk) to process a complex request being made by a client or not being able to keep up with the volume of requests made against the project. ### 54X project errors diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx index 940c7afc7d4..eba2626d045 100644 --- a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx +++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-cpu-charts-9JSlkC.mdx @@ -7,7 +7,7 @@ keywords = [ "cpu", "grafana", "metrics" ] database_id = "ef05da0a-f8bc-44a4-9719-5ae811dba104" --- -> [Guide](/docs/guides/monitoring-troubleshooting/metrics#deploying-supabase-grafana) for setting up Supabase Grafana +> [Guide](/docs/guides/telemetry/metrics/grafana-self-hosted) for setting up Supabase Grafana ## CPU @@ -35,5 +35,5 @@ As the CPU peaks towards 100%, queries and database tasks will begin to throttle 1. [Optimize your queries](/docs/guides/database/query-optimization). 2. [Add indexes](https://github.com/orgs/supabase/discussions/22449) if possible. -3. [Increasing the compute size](/docs/guides/platform/compute-add-ons) +3. [Increasing the compute size](/docs/guides/platform/compute-and-disk) 4. [Distribute load by using read-replicas](/dashboard/project/_/settings/infrastructure) diff --git a/apps/docs/content/troubleshooting/issues-serving-edge-functions-locally.mdx b/apps/docs/content/troubleshooting/issues-serving-edge-functions-locally.mdx index 518f0df49f7..a7abadc9847 100644 --- a/apps/docs/content/troubleshooting/issues-serving-edge-functions-locally.mdx +++ b/apps/docs/content/troubleshooting/issues-serving-edge-functions-locally.mdx @@ -67,6 +67,6 @@ If the output from these commands does not help resolve the issue, open a suppor ## Additional resources -- [Local development guide](/docs/guides/cli/local-development) +- [Local development guide](/docs/guides/local-development/database-migrations) - [Edge Functions quickstart](/docs/guides/functions/quickstart) - [Debugging Edge Functions](/docs/guides/functions/logging) diff --git a/apps/docs/content/troubleshooting/kong-stops-responding-under-heavy-load-locally.mdx b/apps/docs/content/troubleshooting/kong-stops-responding-under-heavy-load-locally.mdx index cb08ae9aae5..f0afb1baa24 100644 --- a/apps/docs/content/troubleshooting/kong-stops-responding-under-heavy-load-locally.mdx +++ b/apps/docs/content/troubleshooting/kong-stops-responding-under-heavy-load-locally.mdx @@ -58,5 +58,5 @@ KONG_NGINX_WORKER_PROCESSES=auto supabase start ## Additional resources -- [Local development guide](/docs/guides/cli/local-development) +- [Local development guide](/docs/guides/local-development/database-migrations) - [CLI repository](https://github.com/supabase/cli) diff --git a/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx b/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx index 53f589b8b87..8db9630f2d0 100644 --- a/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx +++ b/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx @@ -106,5 +106,5 @@ They're all intertwined to some extent. If IO, CPU, or Memory are constrained, t - [Using SQLAlchemy with Supabase](https://github.com/orgs/supabase/discussions/27071) - [Supabase and IPv4/IPv6 compatibility ](https://github.com/orgs/supabase/discussions/27034) - [Addressing Max Client Errors](https://github.com/orgs/supabase/discussions/22305) -- [Connecting to your database](/docs/guides/database/connecting-to-postgres#integrations) +- [Connecting to your database](/docs/guides/database/connecting-to-postgres#quickstarts) - [How to Change Max Database Connections](https://github.com/orgs/supabase/discussions/27197) diff --git a/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx b/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx index 6b5ae533118..710d10e270c 100644 --- a/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx +++ b/apps/docs/content/troubleshooting/not-receiving-auth-emails-from-the-supabase-project-OFSNzw.mdx @@ -7,7 +7,7 @@ keywords = [ "smtp", "email", "delivery", "logs", "provider", "firewalls" ] database_id = "98e3b318-ad9a-41f5-ada0-44b61970ab4f" --- -**We strongly recommend configuring your own custom SMTP provider** in your projects to avoid running into issues with sending emails from your project. The built-in email provider is for demonstration purposes only and offers a very low rate limit. You can read more details about the rate limits [here](/docs/guides/platform/going-into-prod#auth-rate-limits). +**We strongly recommend configuring your own custom SMTP provider** in your projects to avoid running into issues with sending emails from your project. The built-in email provider is for demonstration purposes only and offers a very low rate limit. You can read more details about the rate limits [here](/docs/guides/deployment/going-into-prod#auth-rate-limits). If you're testing your Supabase projects, consider configuring an email testing tool like [Mailtrap](https://mailtrap.io/) to test your emails. It's an email sandbox tool that helps debug emails without sending an email to an end user. diff --git a/apps/docs/content/troubleshooting/pausing-pro-projects-vNL-2a.mdx b/apps/docs/content/troubleshooting/pausing-pro-projects-vNL-2a.mdx index 0dbb540a781..dd60650ca5d 100644 --- a/apps/docs/content/troubleshooting/pausing-pro-projects-vNL-2a.mdx +++ b/apps/docs/content/troubleshooting/pausing-pro-projects-vNL-2a.mdx @@ -16,7 +16,7 @@ If a project is under 500MB, you can [transfer it to be under a free organizatio Alternatively, you can download a [daily backup](/dashboard/project/_/database/backups/scheduled) of only your database for archiving. You can also manually download a .SQL file of your database and storage buckets by following this [guide](/docs/guides/platform/migrating-within-supabase/backup-restore). -You can also download your storage buckets with the [Supabase CLI:](/docs/guides/cli/getting-started?queryGroups=platform&platform=npx) +You can also download your storage buckets with the [Supabase CLI:](/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=npx) ```sql npx supabase login diff --git a/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx index 5269a1661dc..9223be77fd3 100644 --- a/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx +++ b/apps/docs/content/troubleshooting/pgcron-debugging-guide-n1KTaz.mdx @@ -114,7 +114,7 @@ You can view your concurrent peak connection usage throughout the day at the bot Unfortunately, excessive resource strain can slow down or disrupt jobs. -Go to the [reports page](/dashboard/project/_/observability/database) (or [Supabase Grafana](/docs/guides/monitoring-troubleshooting/metrics#deploying-supabase-grafana) if you have it setup), and check for signs of resource exhaustion. If it's clear your database is under pressure, consider upgrading your compute add-on or following the advice from one of the optimization guides: +Go to the [reports page](/dashboard/project/_/observability/database) (or [Supabase Grafana](/docs/guides/telemetry/metrics/grafana-self-hosted) if you have it setup), and check for signs of resource exhaustion. If it's clear your database is under pressure, consider upgrading your compute add-on or following the advice from one of the optimization guides: - [Connections](https://github.com/orgs/supabase/discussions/27141) - [Disk/IO](https://github.com/orgs/supabase/discussions/27003) @@ -152,7 +152,7 @@ order by timestamp desc limit 100; ``` -If you're interested in modifying the query, there is an advanced [guide](https://github.com/orgs/supabase/discussions/26224) for navigating the Postgres logs and a general purpose [one](/docs/guides/platform/advanced-log-filtering) for applying filters. +If you're interested in modifying the query, there is an advanced [guide](https://github.com/orgs/supabase/discussions/26224) for navigating the Postgres logs and a general purpose [one](/docs/guides/telemetry/advanced-log-filtering) for applying filters.
    diff --git a/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx b/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx index e724c1893a9..0409f56edb8 100644 --- a/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx +++ b/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx @@ -59,7 +59,7 @@ where seq_scan + idx_scan > 0 order by n_live_tup desc; ``` -A lot of the [queries for inspecting performance](/docs/reference/cli/supabase-inspect-db) are pre-bundled as part of the [Supabase CLI](/docs/guides/cli/getting-started). For instance, there is a command for testing which indexes of yours are unnecessary and are needlessly taking up space: +A lot of the [queries for inspecting performance](/docs/reference/cli/supabase-inspect-db) are pre-bundled as part of the [Supabase CLI](/docs/guides/local-development/cli/getting-started). For instance, there is a command for testing which indexes of yours are unnecessary and are needlessly taking up space: ```bash npx supabase login diff --git a/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx b/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx index 4106f66cfba..ffe0ad78aec 100644 --- a/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx +++ b/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx @@ -44,7 +44,7 @@ If the cache hit rate begins to drop below the ideal amount, one should consider Optimizing: 1. [Apply indexes](https://github.com/orgs/supabase/discussions/22449): can reduce the amount of data pulled from disk into memory -2. [Increasing the compute size](/docs/guides/platform/compute-add-ons) +2. [Increasing the compute size](/docs/guides/platform/compute-and-disk) 3. [Distribute load by using read-replicas](/dashboard/project/_/settings/infrastructure) 4. [Partitions](/docs/guides/database/partitions): Generally should be used on _very_ large tables to minimize data pulled from disk into memory 5. [Remove bloat](/docs/reference/cli/supabase-inspect-db): Bloat can fragment data across pages, causing redundant data to be pulled from disk. diff --git a/apps/docs/scripts/__snapshots__/build-reference-content.v2.json b/apps/docs/scripts/__snapshots__/build-reference-content.v2.json index d551a84d8f7..a7a073be695 100644 --- a/apps/docs/scripts/__snapshots__/build-reference-content.v2.json +++ b/apps/docs/scripts/__snapshots__/build-reference-content.v2.json @@ -2353,7 +2353,7 @@ { "id": "auth", "title": "Overview", - "notes": "- The auth methods can be accessed via the `supabase.auth` namespace.\n- By default, the supabase client sets `persistSession` to true and attempts to store the session in local storage. When using the supabase client in an environment that doesn't support local storage, you might notice the following warning message being logged:\n\n > No storage option exists to persist the session, which may result in unexpected behavior when using auth. If you want to set `persistSession` to true, please provide a storage option or you may set `persistSession` to false to disable this warning.\n\n This warning message can be safely ignored if you're not using auth on the server-side. If you are using auth and you want to set `persistSession` to true, you will need to provide a custom storage implementation that follows [this interface](https://github.com/supabase/supabase-js/blob/master/packages/core/auth-js/src/lib/types.ts#L1053).\n- Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/platform/going-into-prod#auth-rate-limits) in place to guard against brute force attacks.\n- The expiry of an access token can be set in the \"JWT expiry limit\" field in [your project's auth settings](/dashboard/project/_/auth/providers). A refresh token never expires and can only be used once.\n", + "notes": "- The auth methods can be accessed via the `supabase.auth` namespace.\n- By default, the supabase client sets `persistSession` to true and attempts to store the session in local storage. When using the supabase client in an environment that doesn't support local storage, you might notice the following warning message being logged:\n\n > No storage option exists to persist the session, which may result in unexpected behavior when using auth. If you want to set `persistSession` to true, please provide a storage option or you may set `persistSession` to false to disable this warning.\n\n This warning message can be safely ignored if you're not using auth on the server-side. If you are using auth and you want to set `persistSession` to true, you will need to provide a custom storage implementation that follows [this interface](https://github.com/supabase/supabase-js/blob/master/packages/core/auth-js/src/lib/types.ts#L1053).\n- Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/deployment/going-into-prod#auth-rate-limits) in place to guard against brute force attacks.\n- The expiry of an access token can be set in the \"JWT expiry limit\" field in [your project's auth settings](/dashboard/project/_/auth/providers). A refresh token never expires and can only be used once.\n", "examples": [ { "id": "create-auth-client", diff --git a/apps/docs/spec/cli_v1_config.yaml b/apps/docs/spec/cli_v1_config.yaml index 8dc6cb29e0b..1bf0a040dd7 100644 --- a/apps/docs/spec/cli_v1_config.yaml +++ b/apps/docs/spec/cli_v1_config.yaml @@ -467,7 +467,7 @@ parameters: An array of files or glob patterns to find seeds in. links: - name: 'Seeding your database' - link: 'https://supabase.com/docs/guides/cli/seeding-your-database' + link: 'https://supabase.com/docs/guides/local-development/seeding-your-database' - id: 'realtime.enabled' title: 'realtime.enabled' diff --git a/apps/docs/spec/reference/javascript/v2/partials/auth.json b/apps/docs/spec/reference/javascript/v2/partials/auth.json index 9469067a1e7..edf93343953 100644 --- a/apps/docs/spec/reference/javascript/v2/partials/auth.json +++ b/apps/docs/spec/reference/javascript/v2/partials/auth.json @@ -1,7 +1,7 @@ { "id": "auth", "title": "Overview", - "notes": "- The auth methods can be accessed via the `supabase.auth` namespace.\n- By default, the supabase client sets `persistSession` to true and attempts to store the session in local storage. When using the supabase client in an environment that doesn't support local storage, you might notice the following warning message being logged:\n\n > No storage option exists to persist the session, which may result in unexpected behavior when using auth. If you want to set `persistSession` to true, please provide a storage option or you may set `persistSession` to false to disable this warning.\n\n This warning message can be safely ignored if you're not using auth on the server-side. If you are using auth and you want to set `persistSession` to true, you will need to provide a custom storage implementation that follows [this interface](https://github.com/supabase/supabase-js/blob/master/packages/core/auth-js/src/lib/types.ts#L1053).\n- Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/platform/going-into-prod#auth-rate-limits) in place to guard against brute force attacks.\n- The expiry of an access token can be set in the \"JWT expiry limit\" field in [your project's auth settings](/dashboard/project/_/auth/providers). A refresh token never expires and can only be used once.\n", + "notes": "- The auth methods can be accessed via the `supabase.auth` namespace.\n- By default, the supabase client sets `persistSession` to true and attempts to store the session in local storage. When using the supabase client in an environment that doesn't support local storage, you might notice the following warning message being logged:\n\n > No storage option exists to persist the session, which may result in unexpected behavior when using auth. If you want to set `persistSession` to true, please provide a storage option or you may set `persistSession` to false to disable this warning.\n\n This warning message can be safely ignored if you're not using auth on the server-side. If you are using auth and you want to set `persistSession` to true, you will need to provide a custom storage implementation that follows [this interface](https://github.com/supabase/supabase-js/blob/master/packages/core/auth-js/src/lib/types.ts#L1053).\n- Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/deployment/going-into-prod#auth-rate-limits) in place to guard against brute force attacks.\n- The expiry of an access token can be set in the \"JWT expiry limit\" field in [your project's auth settings](/dashboard/project/_/auth/providers). A refresh token never expires and can only be used once.\n", "examples": [ { "id": "create-auth-client", diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml index c418a59d7e4..9b46a812a72 100644 --- a/apps/docs/spec/supabase_dart_v2.yml +++ b/apps/docs/spec/supabase_dart_v2.yml @@ -277,7 +277,7 @@ functions: name: Sign up with redirect URL isSpotlight: true description: | - - See [redirect URLs and wildcards](/docs/guides/auth#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. code: | ```dart final AuthResponse res = await supabase.auth.signUp( @@ -841,7 +841,7 @@ functions: - id: sign-in-with-sso title: 'signInWithSSO()' notes: | - - Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. + - Before you can call this method you need to [establish a connection](/docs/guides/auth/enterprise-sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. - If you've associated an email domain to the identity provider, you can use the `domain` property to start a sign-in flow. - In case you need to use a different way to start the authentication flow with an identity provider, you can use the `providerId` property. For example: - Mapping specific user email addresses with an identity provider. @@ -1738,8 +1738,8 @@ functions: notes: | - `setSession()` takes in a refresh token and uses it to get a new session. - The refresh token can only be used once to obtain a new session. - - [Refresh token rotation](/docs/guides/cli/config#auth.enable_refresh_token_rotation) is enabled by default on all projects to guard against replay attacks. - - You can configure the [`REFRESH_TOKEN_REUSE_INTERVAL`](/docs/guides/cli/config#auth.refresh_token_reuse_interval) which provides a short window in which the same refresh token can be used multiple times in the event of concurrency or offline issues. + - [Refresh token rotation](/docs/guides/local-development/cli/config#auth.enable_refresh_token_rotation) is enabled by default on all projects to guard against replay attacks. + - You can configure the [`REFRESH_TOKEN_REUSE_INTERVAL`](/docs/guides/local-development/cli/config#auth.refresh_token_reuse_interval) which provides a short window in which the same refresh token can be used multiple times in the event of concurrency or offline issues. params: - name: refreshToken isOptional: false diff --git a/apps/docs/spec/supabase_js_v1.yml b/apps/docs/spec/supabase_js_v1.yml index a5caefbfdce..14946e10246 100644 --- a/apps/docs/spec/supabase_js_v1.yml +++ b/apps/docs/spec/supabase_js_v1.yml @@ -198,7 +198,7 @@ functions: Sometimes you want to control where the user is redirected to after they are logged in. Supabase supports this for any URL path on your website (the URL must either be on the same domain as your [Site URL](https://supabase.com/dashboard/project/_/auth/url-configuration) or match one of the Redirect URLs). - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. code: | ```js const { user, session, error } = await supabase.auth.signIn({ @@ -569,7 +569,7 @@ functions: Sends a password reset request to an email address. - When the user clicks the reset link in the email they are redirected back to your application. You can configure the URL that the user is redirected to via the `redirectTo` param. - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - After the user has been redirected successfully, prompt them for a new password and call `updateUser()`: ```js const { data, error } = await supabase.auth.update({ @@ -1122,7 +1122,7 @@ functions: title: 'on().subscribe()' $ref: '@supabase/supabase-js.lib/SupabaseQueryBuilder.SupabaseQueryBuilder.on' notes: | - - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/database/api#managing-realtime). + - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/api). - If you want to receive the "previous" data for updates and deletes, you will need to set `REPLICA IDENTITY` to `FULL`, like this: `ALTER TABLE your_table REPLICA IDENTITY FULL;` examples: - id: listen-to-all-database-changes diff --git a/apps/docs/spec/supabase_js_v2.yml b/apps/docs/spec/supabase_js_v2.yml index 8a36f7ebf9b..b3592498c16 100644 --- a/apps/docs/spec/supabase_js_v2.yml +++ b/apps/docs/spec/supabase_js_v2.yml @@ -23,7 +23,7 @@ functions: > No storage option exists to persist the session, which may result in unexpected behavior when using auth. If you want to set `persistSession` to true, please provide a storage option or you may set `persistSession` to false to disable this warning. This warning message can be safely ignored if you're not using auth on the server-side. If you are using auth and you want to set `persistSession` to true, you will need to provide a custom storage implementation that follows [this interface](https://github.com/supabase/supabase-js/blob/master/packages/core/auth-js/src/lib/types.ts#L1053). - - Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/platform/going-into-prod#auth-rate-limits) in place to guard against brute force attacks. + - Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/deployment/going-into-prod#auth-rate-limits) in place to guard against brute force attacks. - The expiry of an access token can be set in the "JWT expiry limit" field in [your project's auth settings](/dashboard/project/_/auth/providers). A refresh token never expires and can only be used once. examples: - id: create-auth-client diff --git a/apps/docs/spec/supabase_kt_v1.yml b/apps/docs/spec/supabase_kt_v1.yml index a2f08e8ade9..e8e869ea71b 100644 --- a/apps/docs/spec/supabase_kt_v1.yml +++ b/apps/docs/spec/supabase_kt_v1.yml @@ -2141,7 +2141,7 @@ functions: - id: sign-up-with-redirect name: Sign up with a redirect URL description: | - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. code: | ```kotlin val user = supabase.gotrue.signUpWith(Email, redirectUrl = "https://example.com") { @@ -2203,7 +2203,7 @@ functions: - If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP. - If you're using phone, you can configure whether you want the user to receive a OTP. - The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/redirect-urls). - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) - Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](https://supabase.com/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`. examples: @@ -2257,7 +2257,7 @@ functions: isSpotlight: false description: | - When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectUrl` parameter. This parameter defaults to the [`SITE_URL`](/docs/guides/auth/redirect-urls). It does not redirect the user immediately after invoking this method. - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - oAuthUrl() provides the URL which needs to be opened in a browser. - The redirectTo URL needs to be setup correctly in your project under Authentication -> URL Configuration -> Redirect URLs. - To see how you can use a custom in-app browser on Android, check our [demo](https://github.com/supabase-community/supabase-kt/tree/development/demos/android-login) on GitHub. @@ -2281,7 +2281,7 @@ functions: title: 'signInWithSSO()' $ref: '@supabase/gotrue-js.GoTrueClient.signInWithSSO' notes: | - - Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. + - Before you can call this method you need to [establish a connection](/docs/guides/auth/enterprise-sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. - If you've associated an email domain to the identity provider, you can use the `SSO.withDomain()` function in the `loginWith` method to start a sign-in flow. - In case you need to use a different way to start the authentication flow with an identity provider, you can use the `SSO.withProviderId` function. For example: - Mapping specific user email addresses with an identity provider. @@ -3109,7 +3109,7 @@ functions: Subscribe to realtime changes in your database. title: 'on().subscribe()' notes: | - - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/database/api#managing-realtime). + - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/api). - If you want to receive the "previous" data for updates and deletes, you will need to set `REPLICA IDENTITY` to `FULL`, like this: `ALTER TABLE your_table REPLICA IDENTITY FULL;` - When using a method with a generic type like `track`, `broadcast` or `broadcastFlow`, you have to provide a [serializable class](/docs/reference/kotlin/installing#serialization) as the type parameter. examples: diff --git a/apps/docs/spec/supabase_kt_v2.yml b/apps/docs/spec/supabase_kt_v2.yml index 00aa31721a2..a42bb368f45 100644 --- a/apps/docs/spec/supabase_kt_v2.yml +++ b/apps/docs/spec/supabase_kt_v2.yml @@ -2748,7 +2748,7 @@ functions: Explain is not enabled by default as it can reveal sensitive information about your database. It's best to only enable this for testing environments but if you wish to enable it for production you can provide additional protection by using a `pre-request` function. - Follow the [Performance Debugging Guide](/docs/guides/api/rest/debugging-performance) to enable the functionality on your project. + Follow the [Performance Debugging Guide](/docs/guides/database/debugging-performance) to enable the functionality on your project. params: - name: analyze isOptional: true @@ -2912,7 +2912,7 @@ functions: - id: sign-up-with-redirect name: Sign up with a redirect URL description: | - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. code: | ```kotlin val user = supabase.auth.signUpWith(Email, redirectUrl = "https://example.com") { @@ -3068,7 +3068,7 @@ functions: - If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP. - If you're using phone, you can configure whether you want the user to receive a OTP. - The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/redirect-urls). - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) - Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](https://supabase.com/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`. params: @@ -3166,7 +3166,7 @@ functions: isSpotlight: false description: | - When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectUrl` parameter. This parameter defaults to the [`SITE_URL`](/docs/guides/auth/redirect-urls). It does not redirect the user immediately after invoking this method. - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - getOAuthUrl() provides the URL which needs to be opened in a browser. - The redirectTo URL needs to be setup correctly in your project under Authentication -> URL Configuration -> Redirect URLs. - To see how you can use a custom in-app browser on Android, check our [demo](https://github.com/supabase-community/supabase-kt/tree/development/demos/android-login) on GitHub. @@ -3190,7 +3190,7 @@ functions: title: 'signInWithSSO()' $ref: '@supabase/gotrue-js.GoTrueClient.signInWithSSO' notes: | - - Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. + - Before you can call this method you need to [establish a connection](/docs/guides/auth/enterprise-sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. - If you've associated an email domain to the identity provider, you can change the `domain` property in the `signInWith(SSO)` method to start a sign-in flow. - In case you need to use a different way to start the authentication flow with an identity provider, you can change the `providerId` property. For example: - Mapping specific user email addresses with an identity provider. @@ -4552,7 +4552,7 @@ functions: Subscribe to realtime changes in your database. title: 'on().subscribe()' notes: | - - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/database/api#managing-realtime). + - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/api). - If you want to receive the "previous" data for updates and deletes, you will need to set `REPLICA IDENTITY` to `FULL`, like this: `ALTER TABLE your_table REPLICA IDENTITY FULL;` - When using a method with a generic type like `track`, `broadcast` or `broadcastFlow`, you have to provide a [serializable class](/docs/reference/kotlin/installing#serialization) as the type parameter. - Presence, Broadcast and Database updates are sent through a [Flow](https://kotlinlang.org/docs/flow.html) diff --git a/apps/docs/spec/supabase_kt_v3.yml b/apps/docs/spec/supabase_kt_v3.yml index e4d058afdfa..90c7ab5cf1a 100644 --- a/apps/docs/spec/supabase_kt_v3.yml +++ b/apps/docs/spec/supabase_kt_v3.yml @@ -2843,7 +2843,7 @@ functions: Explain is not enabled by default as it can reveal sensitive information about your database. It's best to only enable this for testing environments but if you wish to enable it for production you can provide additional protection by using a `pre-request` function. - Follow the [Performance Debugging Guide](/docs/guides/api/rest/debugging-performance) to enable the functionality on your project. + Follow the [Performance Debugging Guide](/docs/guides/database/debugging-performance) to enable the functionality on your project. params: - name: analyze isOptional: true @@ -3007,7 +3007,7 @@ functions: - id: sign-up-with-redirect name: Sign up with a redirect URL description: | - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. code: | ```kotlin val user = supabase.auth.signUpWith(Email, redirectUrl = "https://example.com") { @@ -3164,7 +3164,7 @@ functions: - If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP. - If you're using phone, you can configure whether you want the user to receive a OTP. - The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/redirect-urls). - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) - Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](https://supabase.com/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`. params: @@ -3280,7 +3280,7 @@ functions: isSpotlight: false description: | - When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectUrl` parameter. This parameter defaults to the [`SITE_URL`](/docs/guides/auth/redirect-urls). It does not redirect the user immediately after invoking this method. - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - getOAuthUrl() provides the URL which needs to be opened in a browser. - The redirectTo URL needs to be setup correctly in your project under Authentication -> URL Configuration -> Redirect URLs. - To see how you can use a custom in-app browser on Android, check our [demo](https://github.com/supabase-community/supabase-kt/tree/development/demos/android-login) on GitHub. @@ -3304,7 +3304,7 @@ functions: title: 'signInWithSSO()' $ref: '@supabase/gotrue-js.GoTrueClient.signInWithSSO' notes: | - - Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. + - Before you can call this method you need to [establish a connection](/docs/guides/auth/enterprise-sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. - If you've associated an email domain to the identity provider, you can change the `domain` property in the `signInWith(SSO)` method to start a sign-in flow. - In case you need to use a different way to start the authentication flow with an identity provider, you can change the `providerId` property. For example: - Mapping specific user email addresses with an identity provider. @@ -4877,7 +4877,7 @@ functions: Subscribe to realtime changes in your database. title: 'on().subscribe()' notes: | - - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/database/api#managing-realtime). + - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/api). - If you want to receive the "previous" data for updates and deletes, you will need to set `REPLICA IDENTITY` to `FULL`, like this: `ALTER TABLE your_table REPLICA IDENTITY FULL;` - When using a method with a generic type like `track`, `broadcast` or `broadcastFlow`, you have to provide a [serializable class](/docs/reference/kotlin/installing#serialization) as the type parameter. - Presence, Broadcast and Database updates are sent through a [Flow](https://kotlinlang.org/docs/flow.html) diff --git a/apps/docs/spec/supabase_py_v2.yml b/apps/docs/spec/supabase_py_v2.yml index a6905395a50..a75006f8135 100644 --- a/apps/docs/spec/supabase_py_v2.yml +++ b/apps/docs/spec/supabase_py_v2.yml @@ -112,7 +112,7 @@ functions: notes: | - The auth methods can be accessed via the `supabase.auth` namespace. - By default, the supabase client sets `persist_session` to true and attempts to store the session in memory. - - Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/platform/going-into-prod#auth-rate-limits) in place to guard against brute force attacks. + - Any email links and one-time passwords (OTPs) sent have a default expiry of 24 hours. We have the following [rate limits](/docs/guides/deployment/going-into-prod#auth-rate-limits) in place to guard against brute force attacks. - The expiry of an access token can be set in the "JWT expiry limit" field in [your project's auth settings](/dashboard/project/_/settings/jwt/legacy). A refresh token never expires and can only be used once. - id: sign-up title: 'sign_up()' @@ -891,7 +891,7 @@ functions: - name: captcha_token type: string notes: | - - Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. + - Before you can call this method you need to [establish a connection](/docs/guides/auth/enterprise-sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. - If you've associated an email domain to the identity provider, you can use the `domain` property to start a sign-in flow. - In case you need to use a different way to start the authentication flow with an identity provider, you can use the `provider_id` property. For example: - Mapping specific user email addresses with an identity provider. diff --git a/apps/docs/spec/supabase_swift_v1.yml b/apps/docs/spec/supabase_swift_v1.yml index ebe4b3ddb80..6251a84fa52 100644 --- a/apps/docs/spec/supabase_swift_v1.yml +++ b/apps/docs/spec/supabase_swift_v1.yml @@ -120,7 +120,7 @@ functions: - id: sign-up-with-redirect name: Sign up with a redirect URL description: | - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. code: | ```swift try await supabase.auth.signUp( @@ -169,10 +169,10 @@ functions: - If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP. - If you're using phone, you can configure whether you want the user to receive a OTP. - The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/redirect-urls). - - See [redirect URLs and wildcards](/docs/guides/auth#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`. - When using magic links, specify a `redirectTo` that matches a configured url scheme in your iOS app, so Supabase can correctly redirect back to your app. - - See our [Twilio Phone Auth Guide](/docs/guides/auth/phone-login/twilio) for details about configuring WhatsApp sign in. + - See our [Twilio Phone Auth Guide](/docs/guides/auth/phone-login?showSmsProvider=Twilio) for details about configuring WhatsApp sign in. examples: - id: sign-in-with-email name: Sign in with email @@ -228,7 +228,7 @@ functions: isSpotlight: false description: | - When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectTo` parameter. This parameter defaults to the [`SITE_URL`](/docs/guides/auth/redirect-urls). It does not redirect the user immediately after invoking this method. - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - getOAuthSignInURL() provides the URL which needs to be opened in a SFSafariViewController instance. - The redirectTo URL needs to be setup correctly in your project under Authentication -> URL Configuration -> Redirect URLs. code: | diff --git a/apps/docs/spec/supabase_swift_v2.yml b/apps/docs/spec/supabase_swift_v2.yml index 043346624fa..921e5bde9d5 100644 --- a/apps/docs/spec/supabase_swift_v2.yml +++ b/apps/docs/spec/supabase_swift_v2.yml @@ -286,7 +286,7 @@ functions: name: Sign up with a redirect URL description: | - You can provide a default redirect URL when initializing the client. - - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. code: | ```swift try await supabase.auth.signUp( @@ -471,9 +471,9 @@ functions: - If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP. - If you're using phone, you can configure whether you want the user to receive a OTP. - The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/redirect-urls). - - See [redirect URLs and wildcards](/docs/guides/auth#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - See [redirect URLs and wildcards](/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls) to add additional redirect URLs to your project. - Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`. - - See our [Twilio Phone Auth Guide](/docs/guides/auth/phone-login/twilio) for details about configuring WhatsApp sign in. + - See our [Twilio Phone Auth Guide](/docs/guides/auth/phone-login?showSmsProvider=Twilio) for details about configuring WhatsApp sign in. examples: - id: sign-in-with-email name: Sign in with email @@ -643,7 +643,7 @@ functions: - name: captchaToken type: String notes: | - - Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. + - Before you can call this method you need to [establish a connection](/docs/guides/auth/enterprise-sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. - If you've associated an email domain to the identity provider, you can use the `domain` property to start a sign-in flow. - In case you need to use a different way to start the authentication flow with an identity provider, you can use the `providerId` property. For example: - Mapping specific user email addresses with an identity provider. From e74ccefbb903dd37ae2e22610d173c986e016ee3 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:28:01 -0400 Subject: [PATCH 111/141] fix(docs): add cursor-pointer to tabs and copy controls (#48380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix / polish ## What is the current behavior? Some docs interactive controls (copy buttons, tab triggers) do not show a pointer cursor on hover, so they feel less clickable than surrounding links. Called out on https://github.com/supabase/supabase/pull/48318. ## What is the new behavior? Adds `cursor-pointer` in one sweep so related controls stay consistent: - Shared `TabsTrigger` in `packages/ui` (covers PromptPanel AI Prompt / CLI tabs, including the unselected tab) - PromptPanel copy button and Show more / Show less - Guides sidebar “Copy as Markdown” - Docs code block copy button ## To test Hover the controls below and confirm the cursor is `pointer` on both selected and unselected tabs, and on copy buttons. ### Docs Preview: https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs - Homepage setup prompt: [AI Prompt / CLI tabs + copy](https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs) - Quickstart prompt: [Show more / Show less + copy](https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs/guides/getting-started/quickstarts/nextjs) - Guide sidebar + code block: [Copy as Markdown + code copy](https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs/guides/database/tables) ### Design system Preview: https://design-system-git-dnywh-docs-cursor-pointer-supabase.vercel.app/design-system - Shared tabs demo: [Account / Password triggers](https://design-system-git-dnywh-docs-cursor-pointer-supabase.vercel.app/design-system/docs/components/tabs) ### UI library Preview: https://ui-library-git-dnywh-docs-cursor-pointer-supabase.vercel.app/ui - Install command package-manager tabs (npm / pnpm / yarn / bun) + copy: [Password-based auth](https://ui-library-git-dnywh-docs-cursor-pointer-supabase.vercel.app/ui/docs/nextjs/password-based-auth) ### Studio (staging) Preview: https://studio-staging-git-dnywh-docs-cursor-pointer-supabase.vercel.app - Auth user panel: open a project → Authentication → Users → select a user → hover Overview / Logs (and related) tabs - Connect sheet: open Connect on a project → hover the install method tabs ### Studio (self-hosted) Preview: https://studio-self-hosted-git-dnywh-docs-cursor-pointer-supabase.vercel.app - Same `TabsTrigger` callsites as Studio staging (user panel / Connect sheet) ### WWW Preview: https://zone-www-dot-com-git-dnywh-docs-cursor-pointer-supabase.vercel.app - Blog chart tabs (`PGChart` → shared `TabsTrigger`): [Latency / Number of results / Average latency / Raw data](https://zone-www-dot-com-git-dnywh-docs-cursor-pointer-supabase.vercel.app/blog/postgres-full-text-search-vs-the-rest) (scroll to the Results section) ## Additional context Split out from #48318 so the homepage prompt polish stays focused. Prefer fixing the shared tab trigger rather than only the PromptPanel copy button, otherwise the copy control would show pointer while an unselected CLI tab would not. ## Summary by CodeRabbit * **Style** * Improved hover feedback across the docs UI by adding a pointer cursor to “Copy as Markdown,” code block copy and word-wrap controls, prompt copy buttons, and tab selectors. * Updated cursor styling consistently so interactive controls better communicate clickability. --- apps/docs/components/GuidesSidebar.tsx | 2 +- apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx | 4 ++-- apps/docs/features/ui/PromptPanel.tsx | 2 +- packages/ui/src/components/shadcn/ui/tabs.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/docs/components/GuidesSidebar.tsx b/apps/docs/components/GuidesSidebar.tsx index 203ba9fedae..a247ca6e912 100644 --- a/apps/docs/components/GuidesSidebar.tsx +++ b/apps/docs/components/GuidesSidebar.tsx @@ -76,7 +76,7 @@ function AiTools({ className }: { className?: string }) {
    - - {additionalFields.length > 0 && ( -
    -
    - {additionalFields.map((x) => ( - ( - - - - - {x.name} - - - field.onChange( - isNaN(event.target.valueAsNumber) - ? null - : event.target.valueAsNumber - ) - } - /> - - - - )} - /> - ))} -
    -
    -
    - )} -
    - - ) - })} + {columns.map((column, idx) => ( + removeColumn(idx)} + /> + ))} )}
    @@ -362,3 +277,101 @@ export const CreateTableSheet = ({ open, onOpenChange }: CreateTableSheetProps) ) } + +interface ColumnRowProps { + control: Control + idx: number + isCreating: boolean + onRemove: () => void +} + +const ColumnRow = ({ control, idx, isCreating, onRemove }: ColumnRowProps) => { + const columnType = useWatch({ control, name: `columns.${idx}.type` }) + const additionalFields = COLUMN_TYPE_FIELDS[columnType] ?? [] + + return ( +
    + ( + + + + + + )} + /> + ( + + + + )} + /> +
    +
    + + {additionalFields.length > 0 && ( +
    +
    + {additionalFields.map((x) => ( + ( + + + + {x.name} + + field.onChange( + isNaN(event.target.valueAsNumber) ? null : event.target.valueAsNumber + ) + } + /> + + + + )} + /> + ))} +
    +
    +
    + )} +
    + ) +} diff --git a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx index 58d90f95921..38c9cc76269 100644 --- a/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/CreateBucketModal.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' import { useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' +import { SubmitHandler, useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Button, @@ -108,8 +108,8 @@ export const CreateBucketModal = ({ open, onOpenChange }: CreateBucketModalProps }, }) const { formatted_size_limit: formattedSizeLimitError } = form.formState.errors - const isPublicBucket = form.watch('public') - const hasFileSizeLimit = form.watch('has_file_size_limit') + const isPublicBucket = useWatch({ control: form.control, name: 'public' }) + const hasFileSizeLimit = useWatch({ control: form.control, name: 'has_file_size_limit' }) const onSubmit: SubmitHandler = async (values) => { if (!ref) return console.error('Project ref is required') diff --git a/apps/studio/components/interfaces/Storage/EditBucketModal.tsx b/apps/studio/components/interfaces/Storage/EditBucketModal.tsx index cdab844adfe..125d45e2d09 100644 --- a/apps/studio/components/interfaces/Storage/EditBucketModal.tsx +++ b/apps/studio/components/interfaces/Storage/EditBucketModal.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' import { useEffect, useRef, useState } from 'react' -import { useForm, type SubmitHandler } from 'react-hook-form' +import { useForm, useWatch, type SubmitHandler } from 'react-hook-form' import { toast } from 'sonner' import { Button, @@ -121,8 +121,8 @@ export const EditBucketModal = ({ visible, bucket, onClose }: EditBucketModalPro }) const { formatted_size_limit: formattedSizeLimitError } = form.formState.errors - const isPublicBucket = form.watch('public') - const hasFileSizeLimit = form.watch('has_file_size_limit') + const isPublicBucket = useWatch({ control: form.control, name: 'public' }) + const hasFileSizeLimit = useWatch({ control: form.control, name: 'has_file_size_limit' }) const [hasAllowedMimeTypes, setHasAllowedMimeTypes] = useState( Boolean(bucket?.allowed_mime_types?.length) ) diff --git a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx index 3a3bf7ca63f..d93c413405b 100644 --- a/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx +++ b/apps/studio/components/interfaces/Storage/StorageSettings/StorageSettings.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { IS_PLATFORM, useParams } from 'common' import { useEffect, useMemo, useState } from 'react' -import { SubmitHandler, useForm } from 'react-hook-form' +import { SubmitHandler, useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Button, @@ -158,7 +158,7 @@ export const StorageSettings = () => { reValidateMode: 'onSubmit', }) - const { unit: storageUnit } = form.watch() + const storageUnit = useWatch({ control: form.control, name: 'unit' }) const fileSizeLimitError = form.formState.errors.fileSizeLimit const { mutate: updateStorageConfig } = useProjectStorageConfigUpdateUpdateMutation({ diff --git a/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx b/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx index 1d104342edb..813ba728648 100644 --- a/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx +++ b/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { Link2 } from 'lucide-react' import { useEffect } from 'react' import type { SubmitHandler } from 'react-hook-form' -import { useForm } from 'react-hook-form' +import { useForm, useWatch } from 'react-hook-form' import { toast } from 'sonner' import { Button, DialogSectionSeparator, Form, FormControl, FormField, Input } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -51,7 +51,10 @@ export const LinkSupportTicketForm = ({ reValidateMode: 'onBlur', }) - const { category, organizationSlug, projectRef } = form.watch() + const [category, organizationSlug, projectRef] = useWatch({ + control: form.control, + name: ['category', 'organizationSlug', 'projectRef'], + }) const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug const selectedProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef const subscriptionPlanId = getOrgSubscriptionPlan(organizations, selectedOrgSlug) diff --git a/apps/studio/components/interfaces/Support/SupportFormV2.tsx b/apps/studio/components/interfaces/Support/SupportFormV2.tsx index eeb9a7d5bfb..adf6400dd4a 100644 --- a/apps/studio/components/interfaces/Support/SupportFormV2.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormV2.tsx @@ -4,6 +4,7 @@ import { useConstant, useFlag } from 'common' import { CLIENT_LIBRARIES } from 'common/constants' import { type Dispatch, type MouseEventHandler } from 'react' import type { SubmitHandler, UseFormReturn } from 'react-hook-form' +import { useWatch } from 'react-hook-form' import { DialogSectionSeparator, Form } from 'ui' import { @@ -71,7 +72,10 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo const { profile } = useProfile() const respondToEmail = profile?.primary_email ?? 'your email' - const { organizationSlug, projectRef, category, severity, subject, library } = form.watch() + const [organizationSlug, projectRef, category, severity, subject, library] = useWatch({ + control: form.control, + name: ['organizationSlug', 'projectRef', 'category', 'severity', 'subject', 'library'], + }) const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug const selectedProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef diff --git a/apps/studio/components/interfaces/Support/SupportFormV3.tsx b/apps/studio/components/interfaces/Support/SupportFormV3.tsx index d8629c4c5f4..764dc5959eb 100644 --- a/apps/studio/components/interfaces/Support/SupportFormV3.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormV3.tsx @@ -4,6 +4,7 @@ import { useConstant, useFlag } from 'common' import { CLIENT_LIBRARIES } from 'common/constants' import { type Dispatch, type MouseEventHandler } from 'react' import type { SubmitHandler, UseFormReturn } from 'react-hook-form' +import { useWatch } from 'react-hook-form' import { Form, Separator } from 'ui' import { v4 as uuidv4 } from 'uuid' @@ -80,7 +81,10 @@ export const SupportFormV3 = ({ const { profile } = useProfile() const respondToEmail = profile?.primary_email ?? 'your email' - const { organizationSlug, projectRef, category, severity, subject, library } = form.watch() + const [organizationSlug, projectRef, category, severity, subject, library] = useWatch({ + control: form.control, + name: ['organizationSlug', 'projectRef', 'category', 'severity', 'subject', 'library'], + }) const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug const currentProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef From 0833c586acaaca747b7a87c9e4db7f96835714a7 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 30 Jul 2026 12:42:47 +0800 Subject: [PATCH 114/141] fix(studio): use redirect({ to }) for internal TanStack redirects (#48469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hover-preloading any link that points at a redirecting path (e.g. the org invite "Decline" link to `/projects`) hung the tab under the TanStack runtime: `redirect({ href })` is treated as an opaque external target, and the router's preload retry ignores `href` when rebuilding the location, so it re-runs the same `beforeLoad`, throws the same redirect, and recurses forever (TanStack/router#7141 — internal targets must use `to`). **Changed:** - `routes/__root.tsx` — the redirect-table `beforeLoad` splits the destination with `splitInternalUrl()` and throws `redirect({ to, search, hash, statusCode })` instead of `redirect({ href })`. `to` is basepath-relative, so the manual `BASE_PATH` prefix goes away too. - `routes/index.tsx` — same `href` → `to`/`search`/`hash` switch for the `/` redirects; the "targets aren't in the routeTree yet" comment was stale (all three destinations resolve to real routes now). - `OrganizationInvite.tsx` — "Decline" links straight to `/organizations`, skipping the `/projects` redirect hop entirely. ## To test - On the TanStack runtime, hover (don't click) a link to a redirecting path — e.g. the auth overview's "Go to observability" link (`/project/:ref/reports/auth`) or the 404 page's `/projects` link. The page must stay responsive (this hung before). - `/projects` → `/organizations` (307), `/project/:ref/database` → `/database/tables` (308), `/` → `/org`. - Query/hash semantics still hold: `/?next=new-project&projectName=x` → `/new/new-project?projectName=x`; `/project/:ref/database/wrappers?foo=bar` → `/integrations?category=wrapper&foo=bar`; `/org/:slug/invoices#other` → `/org/:slug/billing#invoices`. - Chained redirects stay bounded: `/project/:ref/database/linter` → `/advisors/security` in two hops. All of the above verified locally via Playwright against the TanStack dev server; `redirects.shared` / `internal-url` / compat-router unit tests pass. ## Summary by CodeRabbit - **Bug Fixes** - Fixed the invitation “Decline” action to route users to the Organizations page instead of the Projects page. - Improved Studio redirect/navigation handling by correctly preserving URL search parameters and hash fragments and routing to the intended destination. - **Tests** - Updated Organization Invite test expectations to reflect the corrected “Decline” link destination. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../OrganizationInvite/OrganizationInvite.tsx | 2 +- apps/studio/routes/__root.tsx | 19 +++++++++-- apps/studio/routes/index.tsx | 34 ++++++++++++------- .../components/OrganizationInvite.test.tsx | 2 +- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx index ebffc046a6c..6b150756a5d 100644 --- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx +++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx @@ -194,7 +194,7 @@ export const OrganizationInvite = () => { Accept invite
    diff --git a/apps/studio/routes/__root.tsx b/apps/studio/routes/__root.tsx index 1e5542d5c7f..4d1c42e7c45 100644 --- a/apps/studio/routes/__root.tsx +++ b/apps/studio/routes/__root.tsx @@ -30,6 +30,7 @@ import { Outlet, redirect, Scripts, + type AnyRouter, } from '@tanstack/react-router' import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { @@ -76,6 +77,7 @@ import { AuthProvider } from '@/lib/auth' import { configureMonacoLoader } from '@/lib/configure-monaco-loader' import { API_URL, BASE_PATH, IS_PLATFORM, useDefaultProvider } from '@/lib/constants' import { TimezoneProvider, useTimezone } from '@/lib/datetime' +import { splitInternalUrl } from '@/lib/internal-url' // Custom adapter instead of `nuqs/adapters/tanstack-router` — the stock one // injects a trailing slash before the query on every nuqs write (see module). import { NuqsAdapter } from '@/lib/nuqs-tanstack-adapter' @@ -330,8 +332,21 @@ export const Route = createRootRouteWithContext()({ hash: location.hash, }) if (!match) return - const href = BASE_PATH ? `${BASE_PATH}${match.destination}` : match.destination - throw redirect({ href, statusCode: match.permanent ? 308 : 307 }) + // `to`/`search`/`hash`, never `href`: the router treats `href` as an + // opaque (external) target, and preloading a Link whose beforeLoad + // throws `redirect({ href })` recurses forever — the preload retry + // rebuilds the origin location and re-runs this beforeLoad + // (https://github.com/TanStack/router/issues/7141). `to` is also + // basepath-relative, so no manual BASE_PATH prefix. The explicit + // `search`/`hash` fallbacks clear the incoming values rather than + // inherit them — `preserveQueryAndHash` already merged what carries over. + const { to, search, hash } = splitInternalUrl(match.destination) + throw redirect({ + to, + search: search ?? {}, + hash: hash ?? '', + statusCode: match.permanent ? 308 : 307, + }) }, component: RootComponent, shellComponent: RootDocument, diff --git a/apps/studio/routes/index.tsx b/apps/studio/routes/index.tsx index bafddebe8e4..e4211538c8b 100644 --- a/apps/studio/routes/index.tsx +++ b/apps/studio/routes/index.tsx @@ -1,7 +1,6 @@ -import { createFileRoute, redirect } from '@tanstack/react-router' +import { createFileRoute, redirect, type AnyRouter } from '@tanstack/react-router' import { IS_PLATFORM } from '@/lib/constants' -import { stringifySearch } from '@/lib/router-search-params' // `/` is never rendered — it always redirects. Mirrors the Next.js // `redirects()` rules in next.config.ts: platform sends users to `/org` @@ -16,21 +15,30 @@ export const Route = createFileRoute('/')({ // destination — deep links like `/?next=new-project&projectName=x` must // keep `projectName`. Only the consumed `next` param is dropped (and only // when it matched); everything else passes through. - const suffix = (shouldConsumeNext: boolean) => { - const carried = { ...location.search } as Record - if (shouldConsumeNext) delete carried.next - return `${stringifySearch(carried)}${location.hash ? `#${location.hash}` : ''}` + const carried = (shouldConsumeNext: boolean) => { + const carriedSearch: Record = { ...location.search } + if (shouldConsumeNext) delete carriedSearch.next + return carriedSearch } - // `href` instead of `to` because these targets aren't in the TanStack - // routeTree yet — they're still on the Next.js pages side during the - // migration. Swap to `to` once `/org`, `/new/new-project`, and - // `/project/default` are migrated. + // `to`, never `href`: preloading a Link whose beforeLoad throws + // `redirect({ href })` recurses forever + // (https://github.com/TanStack/router/issues/7141). The `` type arguments opt out of the registered route tree's strict + // typing so the carried free-form search record is accepted. if (IS_PLATFORM) { if (search.next === 'new-project') { - throw redirect({ href: `/new/new-project${suffix(true)}` }) + throw redirect({ + to: '/new/new-project', + search: carried(true), + hash: location.hash, + }) } - throw redirect({ href: `/org${suffix(false)}` }) + throw redirect({ to: '/org', search: carried(false), hash: location.hash }) } - throw redirect({ href: `/project/default${suffix(false)}` }) + throw redirect({ + to: '/project/default', + search: carried(false), + hash: location.hash, + }) }, }) diff --git a/apps/studio/tests/components/OrganizationInvite.test.tsx b/apps/studio/tests/components/OrganizationInvite.test.tsx index 7c0c2ff8fe2..baad458e0e9 100644 --- a/apps/studio/tests/components/OrganizationInvite.test.tsx +++ b/apps/studio/tests/components/OrganizationInvite.test.tsx @@ -156,7 +156,7 @@ describe('OrganizationInvite', () => { expect(screen.getByText('Signed in as')).toBeInTheDocument() expect(screen.getByText('jane@acmecorp.io')).toBeInTheDocument() expect(screen.getByRole('button', { name: 'Accept invite' })).toBeInTheDocument() - expect(screen.getByRole('link', { name: 'Decline' })).toHaveAttribute('href', '/projects') + expect(screen.getByRole('link', { name: 'Decline' })).toHaveAttribute('href', '/organizations') }) test('accepts an invite with the current slug and token', async () => { From d2a3162bf15d8482708695afca28f4db97a65f16 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 30 Jul 2026 18:36:54 +1000 Subject: [PATCH 115/141] Add high availability project creation controls (#48375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Move High Availability into the standard project creation settings above Compute, gated by the `instances.high_availability` entitlement. - Mark the option as Alpha and explain that it is free during Alpha for up to two projects. - Enforce the supported HA configuration: `AWS_K8S`, Postgres 17 on the `ga` release channel (no custom version is sent — the API resolves the image), and the environment-specific local/staging region restrictions. - Show eligible locations in a dedicated **High Availability Regions** group. - Preserve the existing Advanced Configuration availability rules and additionally hide the section while HA is enabled. - Restore the previous provider and Postgres settings when HA is switched off. ## How to test 1. Go to create a new project 2. Ensure you have access to high availability (e.g. on local) 3. Toggle high availability on and note how the project form restricts settings listed above ## Summary by CodeRabbit * **New Features** * Added High Availability to project creation with Alpha warning labeling and improved switch accessibility. * Constrains region selection to compatible High Availability regions and enforces HA-specific engine/release settings. * Disables/hides custom PostgreSQL version selection when High Availability is enabled (and omits HA custom request payloads). * **Bug Fixes** * Improved persistence of selected PostgreSQL version and region across data reloads and configuration panel reopen/toggle. * Restores region when form state temporarily drops values during remounts. * **Tests** * Expanded end-to-end coverage for HA UI, region grouping, and submit/payload restoration behavior. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Joshen Lim --- .../Project/ResumeProjectButton.tsx | 4 +- .../ProjectCreation/HighAvailabilityInput.tsx | 100 +++++- .../InternalOnlyConfiguration.tsx | 41 ++- .../PostgresVersionSelector.tsx | 39 +- .../ProjectCreation.constants.ts | 9 +- .../ProjectCreation.utils.test.ts | 38 ++ .../ProjectCreation/ProjectCreation.utils.ts | 19 + .../ProjectCreation/ProjectCreationForm.tsx | 88 +++-- .../ProjectCreation/RegionSelector.tsx | 98 +++-- apps/studio/tests/pages/new/[slug].test.tsx | 337 ++++++++++++++++-- 10 files changed, 672 insertions(+), 101 deletions(-) create mode 100644 apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts diff --git a/apps/studio/components/interfaces/Project/ResumeProjectButton.tsx b/apps/studio/components/interfaces/Project/ResumeProjectButton.tsx index 0b78c9f0378..5053016ed39 100644 --- a/apps/studio/components/interfaces/Project/ResumeProjectButton.tsx +++ b/apps/studio/components/interfaces/Project/ResumeProjectButton.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useFlag, useParams } from 'common' import { useRouter } from 'next/router' -import { useMemo, useState, type ComponentPropsWithoutRef } from 'react' +import { useMemo, useRef, useState, type ComponentPropsWithoutRef } from 'react' import { useForm } from 'react-hook-form' import { AWS_REGIONS, CloudProvider } from 'shared-data' import { toast } from 'sonner' @@ -99,6 +99,7 @@ export const ResumeProjectButton = ({ mode: 'onChange', defaultValues: { postgresVersionSelection: '' }, }) + const lastValidPostgresVersionSelection = useRef('') const onSelectRestore = () => { if (project?.status !== PROJECT_STATUS.INACTIVE) { @@ -209,6 +210,7 @@ export const ResumeProjectButton = ({ dbRegion={region?.displayName ?? ''} cloudProvider={(project?.cloud_provider ?? 'AWS') as CloudProvider} organizationSlug={selectedOrganization?.slug} + lastValidSelectionRef={lastValidPostgresVersionSelection} /> )} /> diff --git a/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx b/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx index fce289df274..93b8e7ad0f8 100644 --- a/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/HighAvailabilityInput.tsx @@ -1,5 +1,7 @@ +import { useEffect, useRef } from 'react' import { UseFormReturn } from 'react-hook-form' -import { FormControl, FormField, Switch } from 'ui' +import { type CloudProvider } from 'shared-data' +import { Badge, FormControl, FormField, Switch, useWatch } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CreateProjectForm } from './ProjectCreation.schema' @@ -8,10 +10,86 @@ import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' interface HighAvailabilityInputProps { form: UseFormReturn + // Derived from an org-wide regions query owned by the parent form, since fetching it + // here would mean duplicating that query's slug/cloud-provider/instance-size context. + highAvailabilityRegionName: string | undefined } -export const HighAvailabilityInput = ({ form }: HighAvailabilityInputProps) => { +export const HighAvailabilityInput = ({ + form, + highAvailabilityRegionName, +}: HighAvailabilityInputProps) => { + const { getValues, setValue } = form const { hasAccess } = useCheckEntitlements('instances.high_availability') + const highAvailability = useWatch({ control: form.control, name: 'highAvailability' }) + + // Fields to revert to when toggling off HA, so previously selected values aren't lost. + const beforeHighAvailability = useRef<{ + cloudProvider: CloudProvider | undefined + postgresVersionSelection: string | undefined + dbRegion: string | null + }>({ + cloudProvider: undefined, + postgresVersionSelection: undefined, + dbRegion: null, + }) + + const handleHighAvailabilityChange = (checked: boolean) => { + if (checked) { + const currentCloudProvider = getValues('cloudProvider') as CloudProvider + if (currentCloudProvider !== 'AWS_K8S') { + beforeHighAvailability.current.cloudProvider = currentCloudProvider + setValue('cloudProvider', 'AWS_K8S') + } + + beforeHighAvailability.current.postgresVersionSelection = getValues( + 'postgresVersionSelection' + ) + setValue('useOrioleDb', false) + + const currentRegion = getValues('dbRegion') + if ( + highAvailabilityRegionName !== undefined && + currentRegion !== highAvailabilityRegionName + ) { + beforeHighAvailability.current.dbRegion = currentRegion ?? null + setValue('dbRegion', highAvailabilityRegionName) + } + } else { + if (beforeHighAvailability.current.cloudProvider !== undefined) { + setValue('cloudProvider', beforeHighAvailability.current.cloudProvider) + beforeHighAvailability.current.cloudProvider = undefined + } + + if (beforeHighAvailability.current.postgresVersionSelection !== undefined) { + setValue( + 'postgresVersionSelection', + beforeHighAvailability.current.postgresVersionSelection + ) + beforeHighAvailability.current.postgresVersionSelection = undefined + } + + if (beforeHighAvailability.current.dbRegion !== null) { + setValue('dbRegion', beforeHighAvailability.current.dbRegion) + beforeHighAvailability.current.dbRegion = null + } + } + } + + // Catches the case where highAvailabilityRegionName wasn't loaded yet at the moment the + // toggle fired above (the org's available-regions query for AWS_K8S may still be in + // flight). The region auto-fill effect in the parent form skips dirty fields, so a + // manually chosen region would otherwise keep showing in the trigger — force it over + // explicitly once the HA region becomes known. + useEffect(() => { + if (!highAvailability || highAvailabilityRegionName === undefined) return + const currentRegion = getValues('dbRegion') + if (currentRegion === highAvailabilityRegionName) return + if (beforeHighAvailability.current.dbRegion === null) { + beforeHighAvailability.current.dbRegion = currentRegion ?? null + } + setValue('dbRegion', highAvailabilityRegionName) + }, [highAvailability, highAvailabilityRegionName, getValues, setValue]) if (!hasAccess) return null @@ -22,12 +100,24 @@ export const HighAvailabilityInput = ({ form }: HighAvailabilityInputProps) => { name="highAvailability" render={({ field }) => ( + High availability + Alpha +
    + } + description="Horizontally scalable Postgres for highly available deployments. Free during Alpha for up to 2 projects." layout="horizontal" > - + { + handleHighAvailabilityChange(checked) + field.onChange(checked) + }} + /> )} diff --git a/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx b/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx index e1c62c30d4d..3779c2912f7 100644 --- a/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/InternalOnlyConfiguration.tsx @@ -1,7 +1,8 @@ import { useParams } from 'common' +import { useRef } from 'react' import { UseFormReturn } from 'react-hook-form' import { type CloudProvider } from 'shared-data' -import { FormControl, FormField, Input } from 'ui' +import { FormControl, FormField, Input, useWatch } from 'ui' import { CollapsibleCardSection } from 'ui-patterns/CollapsibleCardSection' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -17,6 +18,10 @@ interface InternalOnlyConfigurationProps { export const InternalOnlyConfiguration = ({ form }: InternalOnlyConfigurationProps) => { const { slug } = useParams() const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' + const highAvailability = useWatch({ control: form.control, name: 'highAvailability' }) + // Held here (outside the collapsible content) so the selector's last valid + // selection survives the section being collapsed and reopened. + const lastValidPostgresVersionSelection = useRef('') return ( @@ -36,6 +41,8 @@ export const InternalOnlyConfiguration = ({ form }: InternalOnlyConfigurationPro cloudProvider={form.getValues('cloudProvider') as CloudProvider} organizationSlug={slug} dbRegion={form.getValues('dbRegion')} + disabled={highAvailability} + lastValidSelectionRef={lastValidPostgresVersionSelection} /> )} /> @@ -49,21 +56,23 @@ export const InternalOnlyConfiguration = ({ form }: InternalOnlyConfigurationPro
    - ( - - - - - - )} - /> + {!highAvailability && ( + ( + + + + + + )} + /> + )} form: UseFormReturn + /** + * Owned by the form owner (not this component) so the last valid selection + * survives this selector unmounting, e.g. when its collapsible section + * closes. Create it with useRef('') alongside the form. + */ + lastValidSelectionRef: { current: string } type?: 'create' | 'unpause' layout?: 'vertical' | 'horizontal' label?: string @@ -56,9 +63,11 @@ export const extractPostgresVersionDetails = (value: string): PostgresVersionDet export const PostgresVersionSelector = ({ cloudProvider, dbRegion, + disabled = false, organizationSlug, field, form, + lastValidSelectionRef, type = 'create', layout = 'horizontal', label = 'Postgres version', @@ -96,13 +105,32 @@ export const PostgresVersionSelector = ({ name: 'postgresVersionSelection', }) + // react-hook-form intermittently drops this field's value when its Controller + // remounts, so a one-shot "set the default once versions load" effect leaves + // the select stuck empty. Instead this effect re-asserts off the watched value: + // a selection present in the current list is kept (and remembered in the + // owner-held lastValidSelectionRef), and when the value is missing or cleared + // out from under us it restores the last valid selection, falling back to the + // GA default. useEffect(() => { - if (availableVersions.length > 0) { - const gaVersion = availableVersions.find((x) => x.release_channel === 'ga') - const defaultValue = gaVersion ? formatValue(gaVersion) : formatValue(availableVersions[0]) - form.setValue('postgresVersionSelection', defaultValue) + if (availableVersions.length === 0) return + const isSelectionAvailable = (selection: string) => + availableVersions.some((version) => formatValue(version) === selection) + + if (postgresVersionSelection && isSelectionAvailable(postgresVersionSelection)) { + lastValidSelectionRef.current = postgresVersionSelection + return } - }, [isSuccess, availableVersions, form]) + + if (isSelectionAvailable(lastValidSelectionRef.current)) { + form.setValue('postgresVersionSelection', lastValidSelectionRef.current) + return + } + + const gaVersion = availableVersions.find((x) => x.release_channel === 'ga') + const defaultValue = gaVersion ? formatValue(gaVersion) : formatValue(availableVersions[0]) + form.setValue('postgresVersionSelection', defaultValue) + }, [isSuccess, availableVersions, postgresVersionSelection, lastValidSelectionRef, form]) return ( @@ -110,6 +138,7 @@ export const PostgresVersionSelector = ({ value={postgresVersionSelection} onValueChange={field.onChange} disabled={ + disabled || availableVersions.length === 0 || (type === 'create' && isLoadingProjectCreateVersions) || (type === 'unpause' && isLoadingProjectUnpauseVersions) diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts index 2cdb36b8c86..fb6dcd1d258 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.constants.ts @@ -1,4 +1,11 @@ -import { DesiredInstanceSize } from '@/data/projects/new-project.constants' +import type { + DesiredInstanceSize, + PostgresEngine, + ReleaseChannel, +} from '@/data/projects/new-project.constants' + +export const HIGH_AVAILABILITY_POSTGRES_ENGINE = '17' satisfies PostgresEngine +export const HIGH_AVAILABILITY_RELEASE_CHANNEL = 'ga' satisfies ReleaseChannel // [Joshen] Obtained from https://gist.github.com/tadast/8827699 export const COUNTRY_LAT_LON = { diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts new file mode 100644 index 00000000000..cb820643b13 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' + +import { + HIGH_AVAILABILITY_POSTGRES_ENGINE, + HIGH_AVAILABILITY_RELEASE_CHANNEL, +} from './ProjectCreation.constants' +import { + filterHighAvailabilityRegions, + getHighAvailabilityRegionCode, +} from './ProjectCreation.utils' + +describe('High Availability project creation constraints', () => { + it('pins the Alpha Postgres engine and release channel', () => { + expect(HIGH_AVAILABILITY_POSTGRES_ENGINE).toBe('17') + expect(HIGH_AVAILABILITY_RELEASE_CHANNEL).toBe('ga') + }) + + it.each([ + ['local', 'eu-central-1'], + ['staging', 'us-east-1'], + ['prod', undefined], + ])('resolves the %s region restriction', (environment, expectedRegion) => { + expect(getHighAvailabilityRegionCode(environment)).toBe(expectedRegion) + }) + + it.each([ + ['local', 'eu-central-1'], + ['staging', 'us-east-1'], + ['prod', undefined], + ])('limits %s projects to the required region', (environment, expectedRegion) => { + const regions = [{ code: 'us-east-1' }, { code: 'eu-central-1' }] + + expect(filterHighAvailabilityRegions(regions, true, environment)).toEqual( + expectedRegion === undefined ? regions : [{ code: expectedRegion }] + ) + expect(filterHighAvailabilityRegions(regions, false, environment)).toEqual(regions) + }) +}) diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts index 3b399505d9e..c77d48d680b 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreation.utils.ts @@ -45,3 +45,22 @@ export const monthlyInstancePrice = (instance: string | undefined): number => { export const instanceLabel = (instance: string | undefined): string => { return instanceSizeSpecs[instance as DesiredInstanceSize]?.label || 'Micro' } + +export const getHighAvailabilityRegionCode = ( + environment = process.env.NEXT_PUBLIC_ENVIRONMENT +) => { + if (environment === 'local') return 'eu-central-1' + if (environment === 'staging') return 'us-east-1' + return undefined +} + +export const filterHighAvailabilityRegions = ( + regions: T[], + highAvailability: boolean, + environment = process.env.NEXT_PUBLIC_ENVIRONMENT +) => { + const regionCode = getHighAvailabilityRegionCode(environment) + return highAvailability && regionCode !== undefined + ? regions.filter((region) => region.code === regionCode) + : regions +} diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx index 897ba9b4c8e..4aee33481c3 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx @@ -23,9 +23,14 @@ import { HighAvailabilityInput } from './HighAvailabilityInput' import { InternalOnlyConfiguration } from './InternalOnlyConfiguration' import { OrganizationSelector } from './OrganizationSelector' import { extractPostgresVersionDetails } from './PostgresVersionSelector' -import { sizes } from './ProjectCreation.constants' +import { + HIGH_AVAILABILITY_POSTGRES_ENGINE, + HIGH_AVAILABILITY_RELEASE_CHANNEL, + sizes, +} from './ProjectCreation.constants' import { FormSchema } from './ProjectCreation.schema' import { + getHighAvailabilityRegionCode, instanceLabel, monthlyInstancePrice, smartRegionToExactRegion, @@ -185,6 +190,7 @@ export const ProjectCreationForm = ({ const { dirtyFields } = useFormState(form) const isDbRegionDirty = dirtyFields.dbRegion const smartRegionEnabled = cloudProvider !== 'AWS_NIMBUS' + const highAvailabilityRegionCode = getHighAvailabilityRegionCode() // 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 @@ -265,15 +271,24 @@ export const ProjectCreationForm = ({ } ) + const highAvailabilityRegion = + highAvailability && highAvailabilityRegionCode !== undefined + ? availableRegionsData?.all.specific.find( + (region) => region.code === highAvailabilityRegionCode + ) + : undefined const recommendedSmartRegion = smartRegionEnabled ? availableRegionsData?.recommendations.smartGroup.name : '' const fixedDefaultRegion = PROVIDERS[selectedCloudProvider].default_region.displayName const regionError = smartRegionEnabled ? availableRegionsError : defaultRegionError - const defaultRegion = smartRegionEnabled - ? availableRegionsData?.recommendations.smartGroup.name - : (autoDefaultRegion ?? fixedDefaultRegion) + const defaultRegion = + highAvailability && highAvailabilityRegionCode !== undefined + ? highAvailabilityRegion?.name + : smartRegionEnabled + ? recommendedSmartRegion + : (autoDefaultRegion ?? fixedDefaultRegion) const canCreateProject = isAdmin && !freePlanWithExceedingLimits && !hasOutstandingInvoices const canConfigureGitHubOnCreate = @@ -381,7 +396,11 @@ export const ProjectCreationForm = ({ shouldRunMigrations, } = values - if (postgresVersion && !postgresVersion.match(/1[2-9]\..*/)) { + // HA projects never take a custom version — the API resolves the image from + // postgresEngine + releaseChannel. + const customPostgresVersion = highAvailability ? undefined : postgresVersion + + if (customPostgresVersion && !customPostgresVersion.match(/1[2-9]\..*/)) { return toast.error( `Invalid Postgres version, should start with a number between 12-19, a dot and additional characters, i.e. 15.2 or 15.2.0-3` ) @@ -402,9 +421,19 @@ export const ProjectCreationForm = ({ extractPostgresVersionDetails(postgresVersionSelection) const { smartGroup = [], specific = [] } = availableRegionsData?.all ?? {} - const selectedRegion = smartRegionEnabled - ? (smartGroup.find((x) => x.name === dbRegion) ?? specific.find((x) => x.name === dbRegion)) - : undefined + const selectedRegion = + highAvailability && highAvailabilityRegionCode !== undefined + ? specific.find((region) => region.code === highAvailabilityRegionCode) + : smartRegionEnabled + ? (smartGroup.find((x) => x.name === dbRegion) ?? + specific.find((x) => x.name === dbRegion)) + : undefined + + if (highAvailability && highAvailabilityRegionCode !== undefined && !selectedRegion) { + return toast.error( + `High Availability projects are not available in the required region (${highAvailabilityRegionCode})` + ) + } const parsedGitHubRepositoryId = githubRepositoryId.length > 0 ? Number(githubRepositoryId) : undefined const shouldIncludeGitHubFields = @@ -449,8 +478,16 @@ export const ProjectCreationForm = ({ dataApiExposedSchemas: !dataApi ? [] : undefined, dataApiUseApiSchema: false, dataApiRevokeDefaultPrivileges: dataApi && !dataApiDefaultPrivileges, - postgresEngine: useOrioleDb ? availableOrioleVersion?.postgres_engine : postgresEngine, - releaseChannel: useOrioleDb ? availableOrioleVersion?.release_channel : releaseChannel, + postgresEngine: highAvailability + ? HIGH_AVAILABILITY_POSTGRES_ENGINE + : useOrioleDb + ? availableOrioleVersion?.postgres_engine + : postgresEngine, + releaseChannel: highAvailability + ? HIGH_AVAILABILITY_RELEASE_CHANNEL + : useOrioleDb + ? availableOrioleVersion?.release_channel + : releaseChannel, ...(smartRegionEnabled ? { regionSelection: selectedRegion } : { dbRegion }), ...(shouldIncludeGitHubFields ? { @@ -460,11 +497,11 @@ export const ProjectCreationForm = ({ : {}), } - if (postgresVersion || instanceType) { + if (customPostgresVersion || instanceType) { data['customSupabaseRequest'] = { ami: { - ...(postgresVersion && { - search_tags: { 'tag:postgresVersion': postgresVersion }, + ...(customPostgresVersion && { + search_tags: { 'tag:postgresVersion': customPostgresVersion }, }), ...(instanceType && { instance_type: instanceType }), }, @@ -510,24 +547,12 @@ export const ProjectCreationForm = ({ } }, [defaultRegion, isDbRegionDirty, setValue]) - useEffect(() => { - if (!isDbRegionDirty && recommendedSmartRegion) { - setValue('dbRegion', recommendedSmartRegion) - } - }, [recommendedSmartRegion, isDbRegionDirty, setValue]) - useEffect(() => { if (regionError && fixedDefaultRegion) { resetField('dbRegion', { defaultValue: fixedDefaultRegion }) } }, [regionError, resetField, fixedDefaultRegion]) - useEffect(() => { - if (highAvailability && cloudProvider !== 'AWS_K8S') { - setValue('cloudProvider', 'AWS_K8S') - } - }, [highAvailability, cloudProvider, setValue]) - useEffect(() => { if (watchedInstanceSize !== instanceSize) { setValue('instanceSize', instanceSize, { @@ -657,9 +682,12 @@ export const ProjectCreationForm = ({ )} - {canChooseInstanceSize && } + - + {canChooseInstanceSize && } @@ -674,9 +702,9 @@ export const ProjectCreationForm = ({ {showInternalOnlyConfiguration && } - {showAdvancedConfig && !!availableOrioleVersion && ( - - )} + {showAdvancedConfig && + !!availableOrioleVersion && + highAvailability !== true && } {shouldShowFreeProjectInfo ? ( { const { slug } = useParams() const cloudProvider = form.getValues('cloudProvider') as CloudProvider + const highAvailability = useWatch({ control: form.control, name: 'highAvailability' }) + const dbRegion = useWatch({ control: form.control, name: 'dbRegion' }) + const highAvailabilityRegionCode = getHighAvailabilityRegionCode() const { hasLoaded: flagsLoaded } = useFeatureFlags() const smartRegionEnabled = cloudProvider !== 'AWS_NIMBUS' @@ -91,8 +101,11 @@ export const RegionSelector = ({ { enabled: smartRegionEnabled, staleTime: 1000 * 60 * 5 } // 5 minutes ) - const smartRegions = availableRegionsData?.all.smartGroup ?? [] + const allSmartRegions = availableRegionsData?.all.smartGroup ?? [] const allRegions = availableRegionsData?.all.specific ?? [] + const restrictHighAvailabilityRegion = + highAvailability && highAvailabilityRegionCode !== undefined + const smartRegions = highAvailability ? [] : allSmartRegions const recommendedSmartRegions = new Set( [availableRegionsData?.recommendations.smartGroup.code].filter(Boolean) @@ -111,7 +124,11 @@ export const RegionSelector = ({ } }) - const regionOptions = smartRegionEnabled ? allRegions : regionsArray + const unfilteredRegionOptions = smartRegionEnabled ? allRegions : regionsArray + const regionOptions = filterHighAvailabilityRegions( + [...unfilteredRegionOptions], + highAvailability + ) const isLoading = smartRegionEnabled ? isLoadingAvailableRegions : isLoadingDefaultRegion const showNonProdFields = @@ -120,6 +137,32 @@ export const RegionSelector = ({ const allSelectableRegions = [...smartRegions, ...regionOptions] + // react-hook-form intermittently drops this field's value when its Controller + // remounts (e.g. a sibling section mounting/unmounting in the same update, such as + // toggling high availability), so a one-shot effect isn't enough. Instead this effect + // re-asserts off the watched value: a region present in the current list is kept (and + // remembered in lastValidRegionRef), and when it's missing or cleared out from under us + // it restores the last valid region. allSelectableRegions is intentionally omitted from + // deps — it's a new array every render, and comparing it by reference would defeat the + // point of reacting to genuine content changes on every render where they occur. + const lastValidRegionRef = useRef(undefined) + useEffect(() => { + if (allSelectableRegions.length === 0) return + const isRegionAvailable = (name: string | undefined) => + !!name && allSelectableRegions.some((region) => region.name === name) + + if (isRegionAvailable(dbRegion)) { + lastValidRegionRef.current = dbRegion + return + } + + const lastValidRegion = lastValidRegionRef.current + if (lastValidRegion !== undefined && isRegionAvailable(lastValidRegion)) { + form.setValue('dbRegion', lastValidRegion) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dbRegion, form]) + if (isErrorAvailableRegions) { return } @@ -131,9 +174,16 @@ export const RegionSelector = ({ name="dbRegion" render={({ field }) => { const selectedRegion = allSelectableRegions.find((region) => { - return !!region.name && region.name === field.value + return !!region.name && region.name === dbRegion }) + const selectedRegionLabel = selectedRegion?.name + ? getDisplayNameForSmartRegion(selectedRegion.name) + : dbRegion + const triggerLabel = isLoadingAvailableRegions + ? 'Loading available regions...' + : selectedRegionLabel + const affectingIncidents = incidents.filter((incident) => { const affectedRegions = incident.cache?.affected_regions ?? [] if (affectedRegions.length === 0 || selectedRegion?.code === undefined) return false @@ -156,21 +206,28 @@ export const RegionSelector = ({ description={ <>

    Select the region closest to your users for the best performance.

    - {showNonProdFields && ( + {restrictHighAvailabilityRegion ? (
    -

    Only these regions are supported for local/staging projects:

    -
      -
    • East US (North Virginia)
    • -
    • Central EU (Frankfurt)
    • -
    • Southeast Asia (Singapore)
    • -
    + High Availability projects are currently limited to{' '} + {regionOptions[0]?.name ?? highAvailabilityRegionCode}.
    + ) : ( + showNonProdFields && ( +
    +

    Only these regions are supported for local/staging projects:

    +
      +
    • East US (North Virginia)
    • +
    • Central EU (Frankfurt)
    • +
    • Southeast Asia (Singapore)
    • +
    +
    + ) )} } > - - {field.value !== undefined && ( + {dbRegion !== undefined && (
    + {isLoadingAvailableRegions && ( + + )} {selectedRegion?.code && ( // For some reason, Safari considered the empty string alt text on this icon as misspelled (with VoiceOver) // Only way to fix it is to set the role. Not needed for the combobox options @@ -194,17 +254,13 @@ export const RegionSelector = ({ src={`${BASE_PATH}/img/regions/${selectedRegion.code}.svg`} /> )} - - {selectedRegion?.name - ? getDisplayNameForSmartRegion(selectedRegion.name) - : field.value} - + {triggerLabel}
    )}
    - {smartRegionEnabled && ( + {smartRegionEnabled && !highAvailability && ( <> General regions @@ -244,7 +300,9 @@ export const RegionSelector = ({ )} - Specific regions + + {highAvailability ? 'High Availability Regions' : 'Specific regions'} + {regionOptions.map((value) => { return ( { }) describe('postgres version and orioledb', () => { + test('hides advanced configuration when orioledb is unavailable', async () => { + mockWizardEndpoints() + const onAvailableVersionsRequest = vi.fn() + addAPIMock({ + method: 'post', + path: '/platform/organizations/:slug/available-versions', + response: () => { + onAvailableVersionsRequest() + return HttpResponse.json<{ available_versions: AvailableVersion[] }>({ + available_versions: DEFAULT_AVAILABLE_VERSIONS.available_versions.slice(0, 1), + }) + }, + }) + + await renderWizard() + + await waitFor(() => expect(onAvailableVersionsRequest).toHaveBeenCalled()) + expect( + screen.queryByRole('button', { name: 'Advanced Configuration' }) + ).not.toBeInTheDocument() + }) + test('selecting orioledb shows the alpha warning and submits the oriole engine/channel', async () => { mockWizardEndpoints() const onRequest = vi.fn() @@ -520,7 +555,7 @@ describe('project creation wizard', () => { expect(body.release_channel).toBe('alpha') }) - test('rejects high availability combined with orioledb', async () => { + test('hides advanced configuration only while high availability is enabled', async () => { mockWizardEndpoints() const onRequest = vi.fn() mockCreateProject(onRequest) @@ -528,32 +563,101 @@ describe('project creation wizard', () => { await renderWizard() await fillProjectName('HA Oriole Project') + await generateAndWaitForStrongPassword() await selectRegion(/Americas/) - // The high availability toggle is now a top-level field, gated only by the entitlement. - await user.click(await screen.findByRole('switch')) - fireEvent.click(await screen.findByRole('button', { name: 'Advanced Configuration' })) await user.click(await screen.findByRole('radio', { name: /Postgres with OrioleDB/ })) - await screen.findByText('High availability is not supported with OrioleDB images') - expect(onRequest).not.toHaveBeenCalled() + const highAvailabilitySwitch = await screen.findByRole('switch') + await user.click(highAvailabilitySwitch) + + expect( + screen.queryByRole('button', { name: 'Advanced Configuration' }) + ).not.toBeInTheDocument() + + await user.click(highAvailabilitySwitch) + expect( + await screen.findByRole('button', { name: 'Advanced Configuration' }) + ).toBeInTheDocument() + + await user.click(highAvailabilitySwitch) + fireEvent.click(screen.getByRole('button', { name: 'Create new project' })) + await waitFor(() => expect(onRequest).toHaveBeenCalled()) + expect(onRequest.mock.calls[0][0].postgres_engine).toBe('17') + expect(onRequest.mock.calls[0][0].release_channel).toBe('ga') + }) + + test('keeps a manually selected Postgres version when available versions refetch', async () => { + mockWizardEndpoints({ availableRegions: AVAILABLE_REGIONS_WITH_FRANKFURT }) + const onAvailableVersionsRequest = vi.fn() + addAPIMock({ + method: 'post', + path: '/platform/organizations/:slug/available-versions', + response: () => { + onAvailableVersionsRequest() + return HttpResponse.json<{ available_versions: AvailableVersion[] }>( + DEFAULT_AVAILABLE_VERSIONS + ) + }, + }) + + await renderWizard({ flags: { newProjectInternalOnlyConfiguration: true } }) + + await fillProjectName('Sticky Version Project') + await selectRegion(/Americas/) + fireEvent.click(await screen.findByRole('button', { name: 'Internal-only Configuration' })) + + // Auto-selects the GA default once versions load + await waitFor(() => + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('15.6.1.139') + ) + + await user.click(screen.getByLabelText('Postgres version')) + await user.click(await screen.findByRole('option', { name: /17\.9\.9\.999/ })) + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('17.9.9.999') + + const requestsBeforeRegionChange = onAvailableVersionsRequest.mock.calls.length + await selectRegion(/Frankfurt/) + await waitFor(() => + expect(onAvailableVersionsRequest.mock.calls.length).toBeGreaterThan( + requestsBeforeRegionChange + ) + ) + + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('17.9.9.999') + }) + + test('keeps a non-default Postgres version when the internal panel is collapsed and reopened', async () => { + mockWizardEndpoints() + + await renderWizard({ flags: { newProjectInternalOnlyConfiguration: true } }) + + await screen.findByPlaceholderText('Project name') + await selectRegion(/Americas/) + const panelToggle = await screen.findByRole('button', { + name: 'Internal-only Configuration', + }) + fireEvent.click(panelToggle) + + await waitFor(() => + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('15.6.1.139') + ) + await user.click(screen.getByLabelText('Postgres version')) + await user.click(await screen.findByRole('option', { name: /17\.9\.9\.999/ })) + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('17.9.9.999') + + fireEvent.click(panelToggle) + await waitFor(() => expect(screen.queryByLabelText('Postgres version')).toBeNull()) + fireEvent.click(panelToggle) + + await waitFor(() => + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('17.9.9.999') + ) }) test('blocks submission with a toast when orioledb becomes unavailable after selection', async () => { - const FRANKFURT = 'Central EU (Frankfurt)' - mockWizardEndpoints({ - availableRegions: { - ...DEFAULT_AVAILABLE_REGIONS, - all: { - ...DEFAULT_AVAILABLE_REGIONS.all, - specific: [ - ...DEFAULT_AVAILABLE_REGIONS.all.specific, - { code: 'eu-central-1', name: FRANKFURT, provider: 'AWS', type: 'specific' }, - ], - }, - }, - }) + mockWizardEndpoints({ availableRegions: AVAILABLE_REGIONS_WITH_FRANKFURT }) const onRequest = vi.fn() mockCreateProject(onRequest) @@ -593,13 +697,19 @@ describe('project creation wizard', () => { }) describe('high availability', () => { - test('shows the high availability toggle from the entitlement, without the internal-only flag', async () => { + test('shows the high availability toggle when the entitlement is enabled', async () => { mockWizardEndpoints() await renderWizard() expect(await screen.findByText('High availability')).toBeInTheDocument() - // The toggle must not depend on the internal-only configuration section. + expect(screen.getByText('Alpha')).toBeInTheDocument() + expect(screen.getByText(/Free during Alpha for up to 2 projects/)).toBeInTheDocument() + expect( + screen + .getByText('High availability') + .compareDocumentPosition(screen.getByText('Compute size')) + ).toBe(Node.DOCUMENT_POSITION_FOLLOWING) expect( screen.queryByRole('button', { name: 'Internal-only Configuration' }) ).not.toBeInTheDocument() @@ -623,7 +733,43 @@ describe('project creation wizard', () => { expect(screen.queryByText('High availability')).not.toBeInTheDocument() }) - test('enabling high availability submits the flag and forces the AWS_K8S cloud provider', async () => { + test('removes the custom Postgres version field while high availability is enabled', async () => { + mockWizardEndpoints() + + await renderWizard({ + flags: { + newProjectInternalOnlyConfiguration: true, + }, + }) + + const highAvailabilitySwitch = await screen.findByRole('switch', { + name: 'Enable high availability', + }) + await user.click(highAvailabilitySwitch) + fireEvent.click(await screen.findByRole('button', { name: 'Internal-only Configuration' })) + + expect(screen.getByLabelText('Postgres version')).toBeDisabled() + expect(screen.queryByPlaceholderText('e.g 17.6.1.104')).not.toBeInTheDocument() + + await user.click(highAvailabilitySwitch) + + expect(await screen.findByPlaceholderText('e.g 17.6.1.104')).toBeInTheDocument() + }) + + test('shows high availability regions in a dedicated group', async () => { + mockWizardEndpoints() + + await renderWizard() + + await user.click(await screen.findByRole('switch', { name: 'Enable high availability' })) + await user.click(getSelectTriggerByLabel('Region')) + + expect(await screen.findByText('High Availability Regions')).toBeInTheDocument() + expect(screen.queryByText('General regions')).not.toBeInTheDocument() + expect(screen.queryByText('Specific regions')).not.toBeInTheDocument() + }) + + test('enabling high availability submits the fixed engine and AWS_K8S provider without a custom version', async () => { mockWizardEndpoints() const onRequest = vi.fn() mockCreateProject(onRequest) @@ -633,7 +779,7 @@ describe('project creation wizard', () => { await fillProjectName('HA Project') await generateAndWaitForStrongPassword() await user.click(await screen.findByRole('switch')) - await selectRegion(/Americas/) + await selectRegion(/East US/) fireEvent.click(screen.getByRole('button', { name: 'Create new project' })) @@ -641,6 +787,151 @@ describe('project creation wizard', () => { const body = onRequest.mock.calls[0][0] expect(body.high_availability).toBe(true) expect(body.cloud_provider).toBe('AWS_K8S') + expect(body.postgres_engine).toBe('17') + expect(body.release_channel).toBe('ga') + expect(body.custom_supabase_internal_requests).toBeUndefined() + }) + + test('forces the high availability region over a manually selected region and restores it', async () => { + vi.stubEnv('NEXT_PUBLIC_ENVIRONMENT', 'staging') + try { + mockWizardEndpoints({ availableRegions: AVAILABLE_REGIONS_WITH_FRANKFURT }) + + await renderWizard() + + await screen.findByPlaceholderText('Project name') + await selectRegion(/Frankfurt/) + expect(getSelectTriggerByLabel('Region')).toHaveTextContent(FRANKFURT) + + const highAvailabilitySwitch = await screen.findByRole('switch', { + name: 'Enable high availability', + }) + await user.click(highAvailabilitySwitch) + + await waitFor(() => + expect(getSelectTriggerByLabel('Region')).toHaveTextContent('East US (North Virginia)') + ) + + await user.click(highAvailabilitySwitch) + + await waitFor(() => expect(getSelectTriggerByLabel('Region')).toHaveTextContent(FRANKFURT)) + } finally { + vi.unstubAllEnvs() + } + }) + + test('restores the Postgres version selection when high availability is toggled off', async () => { + mockWizardEndpoints() + // Mirror staging: no versions offered for the standard provider, so the + // selection only ever gets populated by the AWS_K8S list while HA is on. + addAPIMock({ + method: 'post', + path: '/platform/organizations/:slug/available-versions', + response: async ({ request }) => { + const { provider } = (await request.json()) as { provider: string } + return HttpResponse.json<{ available_versions: AvailableVersion[] }>( + provider === 'AWS_K8S' ? DEFAULT_AVAILABLE_VERSIONS : { available_versions: [] } + ) + }, + }) + const onRequest = vi.fn() + mockCreateProject(onRequest) + + await renderWizard({ flags: { newProjectInternalOnlyConfiguration: true } }) + + await fillProjectName('HA Selection Restore Project') + await generateAndWaitForStrongPassword() + await selectRegion(/Americas/) + fireEvent.click(await screen.findByRole('button', { name: 'Internal-only Configuration' })) + + const highAvailabilitySwitch = await screen.findByRole('switch', { + name: 'Enable high availability', + }) + await user.click(highAvailabilitySwitch) + + // The AWS_K8S versions load and auto-select a default while HA is on + await waitFor(() => + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('15.6.1.139') + ) + + await user.click(highAvailabilitySwitch) + + await waitFor(() => + expect(screen.getByLabelText('Postgres version')).toHaveTextContent( + 'Select a Postgres version for your project' + ) + ) + + fireEvent.click(screen.getByRole('button', { name: 'Create new project' })) + + await waitFor(() => expect(onRequest).toHaveBeenCalled()) + const body = onRequest.mock.calls[0][0] + expect(body.high_availability).toBe(false) + expect(body.postgres_engine).toBeUndefined() + expect(body.release_channel).toBeUndefined() + expect(body.custom_supabase_internal_requests).toBeUndefined() + }) + + test('restores a manually selected Postgres version selection after HA toggle', async () => { + mockWizardEndpoints() + addAPIMock({ + method: 'post', + path: '/platform/organizations/:slug/available-versions', + response: async ({ request }) => { + const { provider } = (await request.json()) as { provider: string } + return HttpResponse.json<{ available_versions: AvailableVersion[] }>( + provider === 'AWS_K8S' + ? { available_versions: DEFAULT_AVAILABLE_VERSIONS.available_versions.slice(0, 1) } + : DEFAULT_AVAILABLE_VERSIONS + ) + }, + }) + + await renderWizard({ flags: { newProjectInternalOnlyConfiguration: true } }) + + await fillProjectName('Manual Selection Restore Project') + await selectRegion(/Americas/) + fireEvent.click(await screen.findByRole('button', { name: 'Internal-only Configuration' })) + + await waitFor(() => + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('15.6.1.139') + ) + await user.click(screen.getByLabelText('Postgres version')) + await user.click(await screen.findByRole('option', { name: /17\.9\.9\.999/ })) + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('17.9.9.999') + + const highAvailabilitySwitch = await screen.findByRole('switch', { + name: 'Enable high availability', + }) + await user.click(highAvailabilitySwitch) + await user.click(highAvailabilitySwitch) + + await waitFor(() => + expect(screen.getByLabelText('Postgres version')).toHaveTextContent('17.9.9.999') + ) + }) + + test('restores the standard Postgres configuration when high availability is disabled', async () => { + mockWizardEndpoints() + const onRequest = vi.fn() + mockCreateProject(onRequest) + + await renderWizard() + + await fillProjectName('Standard Project') + await generateAndWaitForStrongPassword() + const highAvailabilitySwitch = await screen.findByRole('switch') + await user.click(highAvailabilitySwitch) + await user.click(highAvailabilitySwitch) + + fireEvent.click(screen.getByRole('button', { name: 'Create new project' })) + + await waitFor(() => expect(onRequest).toHaveBeenCalled()) + const body = onRequest.mock.calls[0][0] + expect(body.high_availability).toBe(false) + expect(body.cloud_provider).toBe('AWS') + expect(body.postgres_engine).toBeUndefined() + expect(body.custom_supabase_internal_requests).toBeUndefined() }) }) From f9a1d2e983d9ecbffaf430e1f94f27a4cc5a08a4 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 30 Jul 2026 18:37:13 +1000 Subject: [PATCH 116/141] Disable unsupported Studio features for HA projects (#48376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary More gating to support upcoming High Availability projects. - Keep the Recent Branch stat visible on the project home page for HA projects, but disable its interaction, reduce its opacity, and skip the branches query. - Treat Realtime as disabled for HA projects in the service-status dropdown so it does not make the project appear unhealthy or trigger unhealthy polling. - Show the shared unsupported-feature empty state for Custom Domains and skip its query on HA projects. - Disable the Enable Realtime checkbox in the table creation sheet for HA projects. ## Summary by CodeRabbit - **New Features** - Added High Availability–aware behavior across activity stats, service status, custom domains, and table realtime controls. - Introduced reusable Branch value UI that shows an “Unavailable” state in High Availability mode. - Added a dedicated realtime toggle UI that disables interaction and updates helper text when unavailable. - **Bug Fixes** - Ensured realtime is treated as disabled (not unhealthy) in High Availability and prevented realtime enabling/saving. - Reduced unnecessary data fetching by gating addon/custom-domain requests and disabling branch queries. - **Tests** - Added coverage for realtime status resolution, BranchStatValue “Unavailable” rendering, and TableRealtimeToggle behavior. --- .../ProjectHome/ActivityStats.test.tsx | 22 ++++ .../interfaces/ProjectHome/ActivityStats.tsx | 120 +++++++++++++----- .../ProjectHome/ServiceStatus.test.ts | 14 ++ .../interfaces/ProjectHome/ServiceStatus.tsx | 11 +- .../ProjectHome/ServiceStatus.utils.ts | 10 ++ .../CustomDomainConfig/CustomDomainConfig.tsx | 34 ++++- .../TableEditor/TableEditor.test.tsx | 76 +++++++++++ .../TableEditor/TableEditor.tsx | 82 ++++++++---- .../custom-domains/custom-domains-query.ts | 2 +- 9 files changed, 303 insertions(+), 68 deletions(-) create mode 100644 apps/studio/components/interfaces/ProjectHome/ActivityStats.test.tsx create mode 100644 apps/studio/components/interfaces/ProjectHome/ServiceStatus.test.ts create mode 100644 apps/studio/components/interfaces/ProjectHome/ServiceStatus.utils.ts create mode 100644 apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.test.tsx diff --git a/apps/studio/components/interfaces/ProjectHome/ActivityStats.test.tsx b/apps/studio/components/interfaces/ProjectHome/ActivityStats.test.tsx new file mode 100644 index 00000000000..6316987f744 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectHome/ActivityStats.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { BranchStatValue } from './ActivityStats' + +describe('BranchStatValue', () => { + it('shows branches as unavailable for HA projects without cached branch data', () => { + render( + + ) + + expect(screen.getByText('Unavailable')).toBeInTheDocument() + expect(screen.queryByText('No branches')).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/ProjectHome/ActivityStats.tsx b/apps/studio/components/interfaces/ProjectHome/ActivityStats.tsx index 220d9d71063..cc68eb0a6bd 100644 --- a/apps/studio/components/interfaces/ProjectHome/ActivityStats.tsx +++ b/apps/studio/components/interfaces/ProjectHome/ActivityStats.tsx @@ -8,28 +8,91 @@ import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { HighAvailabilityBadge } from './HighAvailabilityBadge' import { ServiceStatus } from './ServiceStatus' import { ComputeBadgeWrapper } from '@/components/ui/ComputeBadgeWrapper' +import { DisableInteraction } from '@/components/ui/DisableInteraction' import { SingleStat } from '@/components/ui/SingleStat' import { useBranchesQuery } from '@/data/branches/branches-query' import { useBackupsQuery } from '@/data/database/backups-query' import { DatabaseMigration, useMigrationsQuery } from '@/data/database/migrations-query' import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query' import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { PROJECT_STATUS } from '@/lib/constants' import { EMPTY_ARR } from '@/lib/void' +interface BranchStatValueProps { + currentBranch?: { created_at?: string } + isDefaultProject: boolean + isError: boolean + isHighAvailability: boolean + isLoading: boolean + latestNonDefaultBranch?: { name?: string } +} + +export const BranchStatValue = ({ + currentBranch, + isDefaultProject, + isError, + isHighAvailability, + isLoading, + latestNonDefaultBranch, +}: BranchStatValueProps) => { + if (isHighAvailability) { + return

    Unavailable

    + } + + if (isLoading) { + return + } + + if (isError) { + return

    Unable to load

    + } + + if (isDefaultProject) { + return ( +

    + {latestNonDefaultBranch?.name ?? 'No branches'} +

    + ) + } + + if (currentBranch?.created_at) { + return ( + + ) + } + + return

    Unknown

    +} + export const ActivityStats = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() + const { isHighAvailability } = useHighAvailability() const { data: organization } = useSelectedOrganizationQuery() const { data: resourceWarnings } = useResourceWarningsQuery({ slug: organization?.slug }) const projectResourceWarnings = resourceWarnings?.find((warning) => warning.project === ref) const parentProjectRef = project?.parent_project_ref ?? project?.ref - const { data: branchesData, isPending: isLoadingBranches } = useBranchesQuery({ - projectRef: parentProjectRef, - }) + const { + data: branchesData, + isPending: isLoadingBranches, + isError: isBranchesError, + } = useBranchesQuery( + { + projectRef: parentProjectRef, + }, + { enabled: !isHighAvailability } + ) const isDefaultProject = project?.parent_project_ref === undefined const currentBranch = useMemo( () => (branchesData ?? []).find((b) => b.project_ref === ref), @@ -132,38 +195,27 @@ export const ActivityStats = () => { } /> - } - label={{isDefaultProject ? 'Recent branch' : 'Branch Created'}} - trackingProperties={{ - stat_type: 'branches', - stat_value: branchesData?.length ?? 0, - }} - value={ - isLoadingBranches ? ( - - ) : isDefaultProject ? ( -

    - {latestNonDefaultBranch?.name ?? 'No branches'} -

    - ) : currentBranch?.created_at ? ( - + } + label={{isDefaultProject ? 'Recent branch' : 'Branch Created'}} + trackingProperties={{ + stat_type: 'branches', + stat_value: branchesData?.length ?? 0, + }} + value={ + - ) : ( -

    Unknown

    - ) - } - /> + } + /> + { + it('marks Realtime as disabled on High Availability projects', () => { + expect(resolveRealtimeServiceStatus(true, 'UNHEALTHY')).toBe('DISABLED') + }) + + it('preserves Realtime health on standard projects', () => { + expect(resolveRealtimeServiceStatus(false, 'ACTIVE_HEALTHY')).toBe('ACTIVE_HEALTHY') + expect(resolveRealtimeServiceStatus(false, 'UNHEALTHY')).toBe('UNHEALTHY') + }) +}) diff --git a/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx b/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx index 369eb5d88f7..570778754e2 100644 --- a/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx +++ b/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx @@ -5,23 +5,22 @@ import Link from 'next/link' import { cn, HoverCard, HoverCardContent, HoverCardTrigger, InfoIcon } from 'ui' import { useUnifiedLogsPreview } from '../App/FeaturePreview/FeaturePreviewContext' +import { resolveRealtimeServiceStatus, type ProjectServiceStatus } from './ServiceStatus.utils' import { InlineLink } from '@/components/ui/InlineLink' import { SingleStat } from '@/components/ui/SingleStat' import { useBranchesQuery } from '@/data/branches/branches-query' import { useEdgeFunctionServiceStatusQuery } from '@/data/service-status/edge-functions-status-query' import { useProjectServiceStatusQuery, - type ProjectServiceStatus as APIProjectServiceStatus, type ServiceHealthResponse, } from '@/data/service-status/service-status-query' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' const SERVICE_STATUS_THRESHOLD = 5 // minutes -type ProjectServiceStatus = APIProjectServiceStatus | 'DISABLED' - const iconProps = { size: 18, strokeWidth: 1.5, @@ -101,6 +100,7 @@ const extractDbSchema = (response: ServiceHealthResponse | undefined) => { export const ServiceStatus = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() + const { isHighAvailability } = useHighAvailability() const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview() const { @@ -136,6 +136,9 @@ export const ServiceStatus = () => { refetchInterval: (query) => { const data = query.state.data const isServiceUnhealthy = data?.some((service) => { + if (isHighAvailability && service.name === 'realtime') { + return false + } // if the postgrest service has an empty schema, postgrest has been disabled if (service.name === 'rest' && extractDbSchema(service) === '') { return false @@ -213,7 +216,7 @@ export const ServiceStatus = () => { error: realtimeStatus?.error, docsUrl: undefined, isLoading, - status: realtimeStatus?.status ?? 'UNHEALTHY', + status: resolveRealtimeServiceStatus(isHighAvailability, realtimeStatus?.status), logsUrl: isUnifiedLogsEnabled ? '/logs?filter=log_type:eq:realtime' : '/logs/realtime-logs', diff --git a/apps/studio/components/interfaces/ProjectHome/ServiceStatus.utils.ts b/apps/studio/components/interfaces/ProjectHome/ServiceStatus.utils.ts new file mode 100644 index 00000000000..9affbfb9f27 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectHome/ServiceStatus.utils.ts @@ -0,0 +1,10 @@ +import type { ProjectServiceStatus as APIProjectServiceStatus } from '@/data/service-status/service-status-query' + +export type ProjectServiceStatus = APIProjectServiceStatus | 'DISABLED' + +export const resolveRealtimeServiceStatus = ( + isHighAvailability: boolean, + status?: APIProjectServiceStatus +): ProjectServiceStatus => { + return isHighAvailability ? 'DISABLED' : (status ?? 'UNHEALTHY') +} diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx index 95e1b1f34a4..2afffd1f012 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig.tsx @@ -17,6 +17,7 @@ import { CustomDomainsConfigureHostname } from './CustomDomainsConfigureHostname import { CustomDomainsShimmerLoader } from './CustomDomainsShimmerLoader' import { CustomDomainVerify } from './CustomDomainVerify' import { SupportLink } from '@/components/interfaces/Support/SupportLink' +import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState' import { InlineLinkClassName } from '@/components/ui/InlineLink' import { UpgradeToPro } from '@/components/ui/UpgradeToPro' import { @@ -24,12 +25,14 @@ import { type CustomDomainsData, } from '@/data/custom-domains/custom-domains-query' import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' export const CustomDomainConfig = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() + const { isHighAvailability, isPending: isHighAvailabilityPending } = useHighAvailability() const { data: organization } = useSelectedOrganizationQuery() const isBranch = Boolean(project?.parent_project_ref) const entityLabel = isBranch ? 'branch' : 'project' @@ -37,8 +40,12 @@ export const CustomDomainConfig = () => { const customDomainsDisabledDueToQuota = useFlag('customDomainsDisabledDueToQuota') const plan = organization?.plan?.id + const canLoadCustomDomains = !isHighAvailability && !isHighAvailabilityPending - const { data: addons, isPending: isLoadingAddons } = useProjectAddonsQuery({ projectRef: ref }) + const { data: addons, isPending: isLoadingAddons } = useProjectAddonsQuery( + { projectRef: ref }, + { enabled: canLoadCustomDomains } + ) const hasCustomDomainAddon = !!addons?.selected_addons.find((x) => x.type === 'custom_domain') const { @@ -50,6 +57,7 @@ export const CustomDomainConfig = () => { } = useCustomDomainsQuery( { projectRef: ref }, { + enabled: canLoadCustomDomains, refetchInterval: (query) => { const data = query.state.data // while setting up the ssl certificate, we want to poll every 5 seconds @@ -64,6 +72,28 @@ export const CustomDomainConfig = () => { const { status } = customDomainData || {} + if (isHighAvailability) { + return ( + + + + Custom domains + + Present a branded experience to your users + + + + + + + + ) + } + return ( @@ -75,7 +105,7 @@ export const CustomDomainConfig = () => { - {isLoadingAddons ? ( + {isHighAvailabilityPending || isLoadingAddons ? ( diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.test.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.test.tsx new file mode 100644 index 00000000000..01f45f3a121 --- /dev/null +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.test.tsx @@ -0,0 +1,76 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' + +import { TableRealtimeToggle } from './TableEditor' + +describe('TableRealtimeToggle', () => { + it('disables Realtime and shows the HA-specific description for HA projects', () => { + render( + + ) + + expect(screen.getByRole('checkbox', { name: 'Enable Realtime' })).toBeDisabled() + expect( + screen.getByText('Realtime is unavailable on High Availability projects.') + ).toBeInTheDocument() + expect( + screen.queryByText('Broadcast changes on this table to authorized subscribers.') + ).not.toBeInTheDocument() + }) + + it('keeps Realtime available with the standard description for non-HA projects', () => { + render( + + ) + + expect(screen.getByRole('checkbox', { name: 'Enable Realtime' })).toBeEnabled() + expect( + screen.getByText('Broadcast changes on this table to authorized subscribers.') + ).toBeInTheDocument() + expect( + screen.queryByText('Realtime is unavailable on High Availability projects.') + ).not.toBeInTheDocument() + }) + + it('prevents toggling until HA detection completes', async () => { + const user = userEvent.setup() + const onCheckedChange = vi.fn() + const { rerender } = render( + + ) + + const checkbox = screen.getByRole('checkbox', { name: 'Enable Realtime' }) + expect(checkbox).toBeDisabled() + await user.click(checkbox) + expect(onCheckedChange).not.toHaveBeenCalled() + + rerender( + + ) + + expect(checkbox).toBeEnabled() + await user.click(checkbox) + expect(onCheckedChange).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index b1f32ee4873..8988c2b6518 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -32,6 +32,7 @@ import { useForeignKeyConstraintsQuery } from '@/data/database/foreign-key-const import { useEnumeratedTypesQuery } from '@/data/enumerated-types/enumerated-types-query' import { useCustomContent } from '@/hooks/custom-content/useCustomContent' import { useChanged } from '@/hooks/misc/useChanged' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' @@ -63,6 +64,44 @@ export interface TableEditorProps { apiAccessToggleHandler: TableApiAccessHandlerWithHistoryReturn } +interface TableRealtimeToggleProps { + checked: boolean + isHighAvailability: boolean + isPending: boolean + onCheckedChange: () => void +} + +export const TableRealtimeToggle = ({ + checked, + isHighAvailability, + isPending, + onCheckedChange, +}: TableRealtimeToggleProps) => { + return ( +
    + +
    + +

    + {isHighAvailability + ? 'Realtime is unavailable on High Availability projects.' + : 'Broadcast changes on this table to authorized subscribers.'} +

    +
    +
    + ) +} + export const TableEditor = ({ table, isDuplicating, @@ -77,6 +116,7 @@ export const TableEditor = ({ const snap = useTableEditorStateSnapshot() const tableEditorApi = useContext(TableEditorStateContext) const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all']) + const { isHighAvailability, isPending: isHighAvailabilityPending } = useHighAvailability() const { docsRowLevelSecurityGuidePath } = useCustomContent(['docs:row_level_security_guide_path']) const [params, setParams] = useUrlState() @@ -220,7 +260,7 @@ export const TableEditor = ({ tableId: table?.id, importContent, isRLSEnabled: tableFields.isRLSEnabled, - isRealtimeEnabled: tableFields.isRealtimeEnabled, + isRealtimeEnabled: !isHighAvailability && tableFields.isRealtimeEnabled, isDuplicateRows: isDuplicateRows, existingForeignKeyRelations: foreignKeys, primaryKey, @@ -500,32 +540,20 @@ export const TableEditor = ({ )} {realtimeEnabled && ( -
    - { - track('realtime_toggle_table_clicked', { - newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled', - origin: 'tableSidePanel', - }) - onUpdateField({ - isRealtimeEnabled: !tableFields.isRealtimeEnabled, - }) - }} - /> -
    - -

    - Broadcast changes on this table to authorized subscribers. -

    -
    -
    + { + track('realtime_toggle_table_clicked', { + newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled', + origin: 'tableSidePanel', + }) + onUpdateField({ + isRealtimeEnabled: !tableFields.isRealtimeEnabled, + }) + }} + /> )} diff --git a/apps/studio/data/custom-domains/custom-domains-query.ts b/apps/studio/data/custom-domains/custom-domains-query.ts index b3fe24ba2fd..f10416f63b7 100644 --- a/apps/studio/data/custom-domains/custom-domains-query.ts +++ b/apps/studio/data/custom-domains/custom-domains-query.ts @@ -110,7 +110,7 @@ export const useCustomDomainsQuery = ( ...options }: UseCustomQueryOptions = {} ) => { - const { data } = useProjectAddonsQuery({ projectRef }) + const { data } = useProjectAddonsQuery({ projectRef }, { enabled: enabled !== false }) const hasCustomDomainsAddon = !!data?.selected_addons.find((x) => x.type === 'custom_domain') return useQuery({ From ddc1f4175f1a60092b6e05b3ded0fce49e5ce1e8 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 30 Jul 2026 18:37:45 +1000 Subject: [PATCH 117/141] HA continued surface coverage (#48425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updates connection pooling settings to be read-only when a high availability project with ip4 admonition removed - Updates organization usage to be disabled until supported - Updates database publications to be disabled until supported ## Summary by CodeRabbit * **New Features** * Added High Availability handling for database publications, usage, and connection pooling. * Publications and usage now display dedicated “unavailable” empty states and hide gated content when High Availability is enabled. * Connection pooling switches to managed/read-only mode on High Availability, disabling edits and form submission and disabling related data fetching. * Pooling mode controls are hidden on High Availability. * **Refactor** * Refactored database publications pages by extracting main rendering logic into internal components. * **Tests** * Added Vitest + React Testing Library coverage for High Availability behavior across publications availability, usage, connection pooling, and pooling modes. --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../PublicationsAvailability.test.tsx | 53 ++++++ .../Publications/PublicationsAvailability.tsx | 23 +++ .../Organization/Usage/Usage.test.tsx | 90 ++++++++++ .../interfaces/Organization/Usage/Usage.tsx | 43 +++-- .../ConnectionPooling.test.tsx | 154 ++++++++++++++++++ .../ConnectionPooling/ConnectionPooling.tsx | 148 ++++++++++++----- .../Database/PoolingModesModal.test.tsx | 84 ++++++++++ .../Settings/Database/PoolingModesModal.tsx | 9 +- .../[ref]/database/publications/[id].tsx | 9 +- .../[ref]/database/publications/index.tsx | 9 +- 10 files changed, 567 insertions(+), 55 deletions(-) create mode 100644 apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.test.tsx create mode 100644 apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.tsx create mode 100644 apps/studio/components/interfaces/Organization/Usage/Usage.test.tsx create mode 100644 apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.test.tsx create mode 100644 apps/studio/components/interfaces/Settings/Database/PoolingModesModal.test.tsx diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.test.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.test.tsx new file mode 100644 index 00000000000..244222531a9 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.test.tsx @@ -0,0 +1,53 @@ +import { render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { PublicationsAvailability } from './PublicationsAvailability' + +const { mockUseHighAvailability } = vi.hoisted(() => ({ + mockUseHighAvailability: vi.fn(), +})) + +vi.mock('@/hooks/misc/useHighAvailability', () => ({ + useHighAvailability: mockUseHighAvailability, +})) + +describe('PublicationsAvailability', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('shows the disabled empty state instead of page content for High Availability projects', () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: true }) + + render( + +
    Publications content
    +
    + ) + + expect( + screen.getByText('Publications unavailable on High Availability projects') + ).toBeInTheDocument() + expect( + screen.getByText( + "We're working to bring publications to High Availability projects. Contact support if this is blocking your work." + ) + ).toBeInTheDocument() + expect(screen.queryByText('Publications content')).not.toBeInTheDocument() + }) + + it('renders page content for non-High Availability projects', () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: false }) + + render( + +
    Publications content
    +
    + ) + + expect(screen.getByText('Publications content')).toBeInTheDocument() + expect( + screen.queryByText('Publications unavailable on High Availability projects') + ).not.toBeInTheDocument() + }) +}) diff --git a/apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.tsx b/apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.tsx new file mode 100644 index 00000000000..d38156ada71 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Publications/PublicationsAvailability.tsx @@ -0,0 +1,23 @@ +import { BookOpen } from 'lucide-react' +import type { PropsWithChildren } from 'react' + +import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' + +export const PublicationsAvailability = ({ children }: PropsWithChildren) => { + const { isHighAvailability } = useHighAvailability() + + if (isHighAvailability) { + return ( +
    + +
    + ) + } + + return children +} diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.test.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.test.tsx new file mode 100644 index 00000000000..9718945acbc --- /dev/null +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.test.tsx @@ -0,0 +1,90 @@ +import { screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { Usage } from './Usage' +import { customRender } from '@/tests/lib/custom-render' + +const { + mockUseAsyncCheckPermissions, + mockUseOrgDailyStatsQuery, + mockUseOrgSubscriptionQuery, + mockUseProjectDetailQuery, +} = vi.hoisted(() => ({ + mockUseAsyncCheckPermissions: vi.fn(), + mockUseOrgDailyStatsQuery: vi.fn(), + mockUseOrgSubscriptionQuery: vi.fn(), + mockUseProjectDetailQuery: vi.fn(), +})) + +vi.mock('common', async (importOriginal) => ({ + ...(await importOriginal()), + useParams: () => ({ slug: 'test-org' }), +})) + +vi.mock('@/hooks/misc/useCheckPermissions', () => ({ + useAsyncCheckPermissions: mockUseAsyncCheckPermissions, +})) + +vi.mock('@/data/analytics/org-daily-stats-query', async (importOriginal) => ({ + ...(await importOriginal()), + useOrgDailyStatsQuery: mockUseOrgDailyStatsQuery, +})) + +vi.mock('@/data/projects/project-detail-query', () => ({ + useProjectDetailQuery: mockUseProjectDetailQuery, +})) + +vi.mock('@/data/subscriptions/org-subscription-query', () => ({ + useOrgSubscriptionQuery: mockUseOrgSubscriptionQuery, +})) + +describe('Usage', () => { + beforeEach(() => { + vi.clearAllMocks() + + mockUseAsyncCheckPermissions.mockReturnValue({ can: true, isLoading: false }) + mockUseProjectDetailQuery.mockReturnValue({ + data: { + ref: 'ha-project', + name: 'HA project', + high_availability: true, + }, + isPending: false, + }) + mockUseOrgSubscriptionQuery.mockReturnValue({ + data: undefined, + error: undefined, + isPending: false, + isError: false, + isSuccess: false, + }) + mockUseOrgDailyStatsQuery.mockReturnValue({ + data: undefined, + error: undefined, + isPending: false, + isError: false, + }) + }) + + it('shows a coming-soon empty state for a High Availability project', () => { + customRender(, { + nuqs: { searchParams: { projectRef: 'ha-project' } }, + }) + + expect(screen.getByText('Usage unavailable on High Availability projects')).toBeInTheDocument() + expect( + screen.getByText('Usage insights for High Availability projects are coming soon.') + ).toBeInTheDocument() + expect(screen.queryByText('Usage filtered by project')).not.toBeInTheDocument() + + expect(mockUseProjectDetailQuery).toHaveBeenCalledWith({ ref: 'ha-project' }) + expect(mockUseOrgSubscriptionQuery).toHaveBeenCalledWith( + { orgSlug: 'test-org' }, + { enabled: false } + ) + expect(mockUseOrgDailyStatsQuery).toHaveBeenCalledWith( + expect.objectContaining({ orgSlug: 'test-org', projectRef: 'ha-project' }), + { enabled: false } + ) + }) +}) diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 436a2894f77..90138f2caec 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -1,7 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs from 'dayjs' -import { Check, ChevronDown } from 'lucide-react' +import { ChartArea, Check, ChevronDown } from 'lucide-react' import Link from 'next/link' import { useQueryState } from 'nuqs' import { useMemo, useState } from 'react' @@ -26,12 +26,14 @@ import { } from '@/components/layouts/Scaffold' import { AlertError } from '@/components/ui/AlertError' import { DateRangePicker } from '@/components/ui/DateRangePicker' +import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState' import { NoPermission } from '@/components/ui/NoPermission' import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector' import { useOrgDailyStatsQuery } from '@/data/analytics/org-daily-stats-query' import { useProjectDetailQuery } from '@/data/projects/project-detail-query' import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { resolveHighAvailability } from '@/hooks/misc/useHighAvailability' import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics' export const Usage = () => { @@ -42,6 +44,12 @@ export const Usage = () => { const [selectedProjectRef, setSelectedProjectRef] = useQueryState('projectRef') const [openProjectSelector, setOpenProjectSelector] = useState(false) + const { data: selectedProject, isPending: isLoadingSelectedProject } = useProjectDetailQuery({ + ref: selectedProjectRef ?? undefined, + }) + const isHighAvailability = resolveHighAvailability(selectedProject) + const canLoadUsage = !selectedProjectRef || (!isLoadingSelectedProject && !isHighAvailability) + const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.BILLING_READ, 'stripe.subscriptions' @@ -53,11 +61,7 @@ export const Usage = () => { isPending: isLoadingSubscription, isError: isErrorSubscription, isSuccess: isSuccessSubscription, - } = useOrgSubscriptionQuery({ orgSlug: slug }) - - const { data: selectedProject } = useProjectDetailQuery({ - ref: selectedProjectRef ?? undefined, - }) + } = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: canLoadUsage }) const billingCycleStart = useMemo(() => { return dayjs.unix(subscription?.current_period_start ?? 0).utc() @@ -107,12 +111,27 @@ export const Usage = () => { error: orgDailyStatsError, isPending: isLoadingOrgDailyStats, isError: isErrorOrgDailyStats, - } = useOrgDailyStatsQuery({ - orgSlug: slug, - projectRef: selectedProjectRef ?? undefined, - startDate, - endDate, - }) + } = useOrgDailyStatsQuery( + { + orgSlug: slug, + projectRef: selectedProjectRef ?? undefined, + startDate, + endDate, + }, + { enabled: canLoadUsage } + ) + + if (isHighAvailability) { + return ( +
    + +
    + ) + } return ( <> diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.test.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.test.tsx new file mode 100644 index 00000000000..491aaf978fe --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.test.tsx @@ -0,0 +1,154 @@ +import { screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { ConnectionPooling } from './ConnectionPooling' +import { customRender } from '@/tests/lib/custom-render' + +const { + mockUpdatePoolerConfig, + mockUseAsyncCheckPermissions, + mockUseCheckEntitlements, + mockUseHighAvailability, + mockUseMaxConnectionsQuery, + mockUsePgbouncerConfigQuery, + mockUsePgbouncerConfigurationUpdateMutation, + mockUseProjectAddonsQuery, + mockUseSelectedProjectQuery, +} = vi.hoisted(() => ({ + mockUpdatePoolerConfig: vi.fn(), + mockUseAsyncCheckPermissions: vi.fn(), + mockUseCheckEntitlements: vi.fn(), + mockUseHighAvailability: vi.fn(), + mockUseMaxConnectionsQuery: vi.fn(), + mockUsePgbouncerConfigQuery: vi.fn(), + mockUsePgbouncerConfigurationUpdateMutation: vi.fn(), + mockUseProjectAddonsQuery: vi.fn(), + mockUseSelectedProjectQuery: vi.fn(), +})) + +vi.mock('common', async (importOriginal) => ({ + ...(await importOriginal()), + useParams: () => ({ ref: 'ha-project' }), +})) + +vi.mock('@/hooks/misc/useCheckPermissions', () => ({ + useAsyncCheckPermissions: mockUseAsyncCheckPermissions, +})) + +vi.mock('@/hooks/misc/useCheckEntitlements', () => ({ + useCheckEntitlements: mockUseCheckEntitlements, +})) + +vi.mock('@/hooks/misc/useHighAvailability', () => ({ + useHighAvailability: mockUseHighAvailability, +})) + +vi.mock('@/hooks/misc/useSelectedProject', () => ({ + useSelectedProjectQuery: mockUseSelectedProjectQuery, +})) + +vi.mock('@/data/database/max-connections-query', () => ({ + useMaxConnectionsQuery: mockUseMaxConnectionsQuery, +})) + +vi.mock('@/data/database/pgbouncer-config-query', () => ({ + usePgbouncerConfigQuery: mockUsePgbouncerConfigQuery, +})) + +vi.mock('@/data/database/pgbouncer-config-update-mutation', () => ({ + usePgbouncerConfigurationUpdateMutation: mockUsePgbouncerConfigurationUpdateMutation, +})) + +vi.mock('@/data/subscriptions/project-addons-query', () => ({ + useProjectAddonsQuery: mockUseProjectAddonsQuery, +})) + +const expectEveryQueryCall = (queryMock: ReturnType, enabled: boolean) => { + expect(queryMock).toHaveBeenCalled() + for (const [, options] of queryMock.mock.calls) { + expect(options).toEqual({ enabled }) + } +} + +describe('ConnectionPooling', () => { + beforeEach(() => { + vi.clearAllMocks() + + mockUseSelectedProjectQuery.mockReturnValue({ + data: { + id: 1, + ref: 'ha-project', + infra_compute_size: 'small', + connectionString: 'postgresql://example', + }, + }) + mockUseHighAvailability.mockReturnValue({ isHighAvailability: true, isPending: false }) + mockUseAsyncCheckPermissions.mockReturnValue({ can: true }) + mockUseCheckEntitlements.mockReturnValue({ hasAccess: true }) + mockUsePgbouncerConfigQuery.mockReturnValue({ + data: undefined, + error: undefined, + isPending: false, + isError: false, + isSuccess: false, + }) + mockUseMaxConnectionsQuery.mockReturnValue({ data: undefined }) + mockUseProjectAddonsQuery.mockReturnValue({ + data: { + selected_addons: [ + { + type: 'compute_instance', + variant: { name: 'Small', identifier: 'ci_small' }, + }, + ], + }, + isSuccess: true, + }) + mockUsePgbouncerConfigurationUpdateMutation.mockReturnValue({ + mutate: mockUpdatePoolerConfig, + isPending: false, + }) + }) + + it('renders High Availability pooling settings as read-only', () => { + customRender() + + expect( + screen.getAllByText( + 'Connection pooling settings are managed automatically on High Availability projects' + ) + ).toHaveLength(1) + expect(screen.getByRole('link', { name: 'Learn more' })).toHaveAttribute( + 'href', + 'https://multigres.com/blog/pooling-without-choosing-a-mode' + ) + + expect(screen.queryByText('Enable IPv4 add-on')).not.toBeInTheDocument() + expect(screen.getByPlaceholderText('Managed automatically')).toBeDisabled() + expect(screen.getByDisplayValue('100000')).toBeDisabled() + expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Save' })).not.toBeInTheDocument() + expect(mockUpdatePoolerConfig).not.toHaveBeenCalled() + + expectEveryQueryCall(mockUsePgbouncerConfigQuery, false) + expectEveryQueryCall(mockUseMaxConnectionsQuery, false) + }) + + it('does not fetch pooling config while the high availability state is pending', () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: false, isPending: true }) + + customRender() + + expectEveryQueryCall(mockUsePgbouncerConfigQuery, false) + expectEveryQueryCall(mockUseMaxConnectionsQuery, false) + }) + + it('fetches pooling config for non high availability projects', () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: false, isPending: false }) + + customRender() + + expectEveryQueryCall(mockUsePgbouncerConfigQuery, true) + expectEveryQueryCall(mockUseMaxConnectionsQuery, true) + }) +}) diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx index 79f46c844e1..d6d3a31638b 100644 --- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx @@ -38,6 +38,7 @@ import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' import { AlertError } from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import { FormActions } from '@/components/ui/Forms/FormActions' +import { HighAvailabilityDisabledSectionNotice } from '@/components/ui/HighAvailability/HighAvailabilityDisabledSectionNotice' import { InlineLink } from '@/components/ui/InlineLink' import Panel from '@/components/ui/Panel' import { useMaxConnectionsQuery } from '@/data/database/max-connections-query' @@ -46,10 +47,14 @@ import { usePgbouncerConfigurationUpdateMutation } from '@/data/database/pgbounc import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' const formId = 'pooling-configuration-form' +const HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS = 100_000 +const HA_DISABLED_TITLE = + 'Connection pooling settings are managed automatically on High Availability projects' const PoolingConfigurationFormSchema = z.object({ default_pool_size: z.preprocess( @@ -68,6 +73,8 @@ const PoolingConfigurationFormSchema = z.object({ export const ConnectionPooling = () => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() + const { isHighAvailability, isPending: isHighAvailabilityPending } = useHighAvailability() + const canLoadPoolingConfig = !isHighAvailability && !isHighAvailabilityPending const { can: canUpdateConnectionPoolingConfiguration } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'projects', @@ -80,15 +87,18 @@ export const ConnectionPooling = () => { isPending: isLoadingPgbouncerConfig, isError: isErrorPgbouncerConfig, isSuccess: isSuccessPgbouncerConfig, - } = usePgbouncerConfigQuery({ projectRef }) + } = usePgbouncerConfigQuery({ projectRef }, { enabled: canLoadPoolingConfig }) const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler') const disablePoolModeSelection = !hasDedicatedPooler - const { data: maxConnData } = useMaxConnectionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) + const { data: maxConnData } = useMaxConnectionsQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { enabled: canLoadPoolingConfig } + ) const { data: addons, isSuccess: isSuccessAddons } = useProjectAddonsQuery({ projectRef }) const { mutate: updatePoolerConfig, isPending: isUpdatingPoolerConfig } = @@ -120,7 +130,7 @@ export const ConnectionPooling = () => { const onSubmit: SubmitHandler> = async (data) => { const { default_pool_size } = data - if (!projectRef) return console.error('Project ref is required') + if (!projectRef || isHighAvailability) return updatePoolerConfig( { @@ -165,7 +175,24 @@ export const ConnectionPooling = () => { - {isSuccessAddons && !disablePoolModeSelection && !hasIpv4Addon && ( + {isHighAvailability && ( + + High Availability projects run one pooler per Postgres pod, each supporting up to{' '} + {HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS.toLocaleString()} active or passive + connections, with pooling behavior selected automatically.{' '} + + Learn more + + . + + } + /> + )} + + {isSuccessAddons && !isHighAvailability && !disablePoolModeSelection && !hasIpv4Addon && ( { resetForm()} - helper={ - !canUpdateConnectionPoolingConfiguration - ? 'You need additional permissions to update connection pooling settings' - : undefined - } - /> + isHighAvailability ? undefined : ( + resetForm()} + helper={ + !canUpdateConnectionPoolingConfiguration + ? 'You need additional permissions to update connection pooling settings' + : undefined + } + /> + ) } > - {isLoadingPgbouncerConfig && ( + {!isHighAvailability && isLoadingPgbouncerConfig && (
    {Array.from({ length: 4 }).map((_, i) => ( @@ -213,31 +242,40 @@ export const ConnectionPooling = () => {
    )} - {isErrorPgbouncerConfig && ( + {!isHighAvailability && isErrorPgbouncerConfig && ( )} - {connectionPoolingUnavailable && ( + {!isHighAvailability && connectionPoolingUnavailable && ( )} - {isSuccessPgbouncerConfig && !connectionPoolingUnavailable && ( + {(isHighAvailability || + (isSuccessPgbouncerConfig && !connectionPoolingUnavailable)) && ( <>
    Connection poolers

    - Configuration is shared across all connection poolers. + {isHighAvailability + ? 'One pooler runs for each Postgres pod in the cluster.' + : 'Configuration is shared across all connection poolers.'}

    - Shared - {!disablePoolModeSelection && Dedicated} + {isHighAvailability ? ( + High Availability + ) : ( + <> + Shared + {!disablePoolModeSelection && Dedicated} + + )}
    @@ -255,11 +293,18 @@ export const ConnectionPooling = () => { layout="flex-row-reverse" label="Connection pool size" description={ -

    - The maximum number of connections made to the underlying Postgres - cluster, per user+db combination. Pool size has a default of{' '} - {defaultPoolSize} based on your compute size of {computeSize}. -

    + isHighAvailability ? ( +

    + Pool size is managed automatically for each Postgres pod and cannot + be changed. +

    + ) : ( +

    + The maximum number of connections made to the underlying Postgres + cluster, per user+db combination. Pool size has a default of{' '} + {defaultPoolSize} based on your compute size of {computeSize}. +

    + ) } className="[&>div]:md:w-1/2 [&>div]:xl:w-2/5 [&>div>div]:w-full" > @@ -267,10 +312,15 @@ export const ConnectionPooling = () => { field.onChange( isNaN(event.target.valueAsNumber) @@ -279,12 +329,15 @@ export const ConnectionPooling = () => { ) } /> - - connections - + {!isHighAvailability && ( + + connections + + )}
    - {!!maxConnData && + {!isHighAvailability && + !!maxConnData && (default_pool_size ?? 15) > maxConnData.maxConnections * 0.8 && ( @@ -313,7 +366,14 @@ export const ConnectionPooling = () => { label="Max client connections" className="[&>div]:md:w-1/2 [&>div]:xl:w-2/5 [&>div>div]:w-full" description={ - <> + isHighAvailability ? ( +

    + Each pooler can support up to{' '} + {HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS.toLocaleString()} active + or passive client connections. This value is managed automatically + and cannot be changed. +

    + ) : (

    The maximum number of concurrent client connections allowed. This value is fixed at {defaultMaxClientConn} based on your compute size @@ -324,7 +384,7 @@ export const ConnectionPooling = () => { Learn more

    - + ) } > @@ -333,8 +393,16 @@ export const ConnectionPooling = () => { {...field} type="number" className="w-full" - value={pgbouncerConfig?.max_client_conn ?? ''} - placeholder={defaultMaxClientConn.toString()} + value={ + isHighAvailability + ? HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS + : (pgbouncerConfig?.max_client_conn ?? '') + } + placeholder={ + isHighAvailability + ? HIGH_AVAILABILITY_MAX_CLIENT_CONNECTIONS.toString() + : defaultMaxClientConn.toString() + } onChange={(event) => field.onChange( isNaN(event.target.valueAsNumber) diff --git a/apps/studio/components/interfaces/Settings/Database/PoolingModesModal.test.tsx b/apps/studio/components/interfaces/Settings/Database/PoolingModesModal.test.tsx new file mode 100644 index 00000000000..3258a0327b1 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Database/PoolingModesModal.test.tsx @@ -0,0 +1,84 @@ +import { screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { PoolingModesModal } from './PoolingModesModal' +import { customRender } from '@/tests/lib/custom-render' + +const { + mockUseDatabaseSelectorStateSnapshot, + mockUseDatabaseSettingsStateSnapshot, + mockUseHighAvailability, + mockUseSupavisorConfigurationQuery, +} = vi.hoisted(() => ({ + mockUseDatabaseSelectorStateSnapshot: vi.fn(), + mockUseDatabaseSettingsStateSnapshot: vi.fn(), + mockUseHighAvailability: vi.fn(), + mockUseSupavisorConfigurationQuery: vi.fn(), +})) + +vi.mock('common', async (importOriginal) => ({ + ...(await importOriginal()), + useParams: () => ({ ref: 'ha-project' }), +})) + +vi.mock('@/hooks/misc/useHighAvailability', () => ({ + useHighAvailability: mockUseHighAvailability, +})) + +vi.mock('@/data/database/supavisor-configuration-query', () => ({ + useSupavisorConfigurationQuery: mockUseSupavisorConfigurationQuery, +})) + +vi.mock('@/state/database-selector', () => ({ + useDatabaseSelectorStateSnapshot: mockUseDatabaseSelectorStateSnapshot, +})) + +vi.mock('@/state/database-settings', () => ({ + useDatabaseSettingsStateSnapshot: mockUseDatabaseSettingsStateSnapshot, +})) + +const expectEveryQueryCall = (queryMock: ReturnType, enabled: boolean) => { + expect(queryMock).toHaveBeenCalled() + for (const [, options] of queryMock.mock.calls) { + expect(options).toEqual({ enabled }) + } +} + +describe('PoolingModesModal', () => { + beforeEach(() => { + vi.clearAllMocks() + + mockUseDatabaseSettingsStateSnapshot.mockReturnValue({ + showPoolingModeHelper: true, + setShowPoolingModeHelper: vi.fn(), + }) + mockUseDatabaseSelectorStateSnapshot.mockReturnValue({ selectedDatabaseId: 'ha-project' }) + mockUseSupavisorConfigurationQuery.mockReturnValue({ data: undefined }) + }) + + it('renders nothing and skips the supavisor query for High Availability projects', () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: true, isPending: false }) + + const { container } = customRender() + + expect(container).toBeEmptyDOMElement() + expectEveryQueryCall(mockUseSupavisorConfigurationQuery, false) + }) + + it('does not fetch supavisor config while the high availability state is pending', () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: false, isPending: true }) + + customRender() + + expectEveryQueryCall(mockUseSupavisorConfigurationQuery, false) + }) + + it('renders the modal and fetches supavisor config for non high availability projects', () => { + mockUseHighAvailability.mockReturnValue({ isHighAvailability: false, isPending: false }) + + customRender() + + expect(screen.getByText('Which pooling mode should I use?')).toBeInTheDocument() + expectEveryQueryCall(mockUseSupavisorConfigurationQuery, true) + }) +}) diff --git a/apps/studio/components/interfaces/Settings/Database/PoolingModesModal.tsx b/apps/studio/components/interfaces/Settings/Database/PoolingModesModal.tsx index d9ef6ce37ac..3e16bc7b8cb 100644 --- a/apps/studio/components/interfaces/Settings/Database/PoolingModesModal.tsx +++ b/apps/studio/components/interfaces/Settings/Database/PoolingModesModal.tsx @@ -19,6 +19,7 @@ import { import { Markdown } from '@/components/interfaces/Markdown' import { DocsButton } from '@/components/ui/DocsButton' import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query' +import { useHighAvailability } from '@/hooks/misc/useHighAvailability' import { DOCS_URL } from '@/lib/constants' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' import { useDatabaseSettingsStateSnapshot } from '@/state/database-settings' @@ -27,8 +28,12 @@ export const PoolingModesModal = () => { const { ref: projectRef } = useParams() const snap = useDatabaseSettingsStateSnapshot() const state = useDatabaseSelectorStateSnapshot() + const { isHighAvailability, isPending: isHighAvailabilityPending } = useHighAvailability() - const { data } = useSupavisorConfigurationQuery({ projectRef: projectRef }) + const { data } = useSupavisorConfigurationQuery( + { projectRef: projectRef }, + { enabled: !isHighAvailability && !isHighAvailabilityPending } + ) const primaryConfig = data?.find((x) => x.identifier === state.selectedDatabaseId) const navigateToPoolerSettings = () => { @@ -36,6 +41,8 @@ export const PoolingModesModal = () => { if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }) } + if (isHighAvailability) return null + return ( diff --git a/apps/studio/pages/project/[ref]/database/publications/[id].tsx b/apps/studio/pages/project/[ref]/database/publications/[id].tsx index 965c22faa2a..8ae7129b577 100644 --- a/apps/studio/pages/project/[ref]/database/publications/[id].tsx +++ b/apps/studio/pages/project/[ref]/database/publications/[id].tsx @@ -4,6 +4,7 @@ import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' +import { PublicationsAvailability } from '@/components/interfaces/Database/Publications/PublicationsAvailability' import { PublicationsTables } from '@/components/interfaces/Database/Publications/PublicationsTables' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' @@ -14,7 +15,7 @@ import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import type { NextPageWithLayout } from '@/types' -const DatabasePublications: NextPageWithLayout = () => { +const DatabasePublicationsContent = () => { const { ref, id } = useParams() const { data: project } = useSelectedProjectQuery() const { can: canViewPublications, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( @@ -54,6 +55,12 @@ const DatabasePublications: NextPageWithLayout = () => { ) } +const DatabasePublications: NextPageWithLayout = () => ( + + + +) + DatabasePublications.getLayout = (page) => ( {page} diff --git a/apps/studio/pages/project/[ref]/database/publications/index.tsx b/apps/studio/pages/project/[ref]/database/publications/index.tsx index ded564a278a..59cd351d97b 100644 --- a/apps/studio/pages/project/[ref]/database/publications/index.tsx +++ b/apps/studio/pages/project/[ref]/database/publications/index.tsx @@ -2,6 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection } from 'ui-patterns/PageSection' +import { PublicationsAvailability } from '@/components/interfaces/Database/Publications/PublicationsAvailability' import { PublicationsList } from '@/components/interfaces/Database/Publications/PublicationsList' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' @@ -10,7 +11,7 @@ import { NoPermission } from '@/components/ui/NoPermission' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from '@/types' -const DatabasePublications: NextPageWithLayout = () => { +const DatabasePublicationsContent = () => { const { can: canViewPublications, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_READ, 'publications' @@ -31,6 +32,12 @@ const DatabasePublications: NextPageWithLayout = () => { ) } +const DatabasePublications: NextPageWithLayout = () => ( + + + +) + DatabasePublications.getLayout = (page) => ( {page} From 4db78dfe5fa81a89ff0b1b4769092616c956ff6b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:49:19 +0200 Subject: [PATCH 118/141] chore(www): update DPA effective date to August 1, 2026 (#48481) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Requested by **Nicole Kramer** · [Slack thread](https://supabase.slack.com/archives/C0161K73J1J/p1785399057853249?thread_ts=1785399057.853249&cid=C0161K73J1J)_ ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Content update — moves the Data Processing Addendum's effective date out by two months. ## What is the current behavior? The DPA page at `/legal/customer-resources/data-processing-addendum` shows "Version 1 — June 1, 2026" in the version selector, and the document body opens with "Last Modified: 1 June 2026". ## What is the new behavior? Both now read August 1, 2026: the version selector shows "Version 1 — August 1, 2026" and the document opens with "Last Modified: 1 August 2026". ## Additional context The page renders a hardcoded `versions` array through the shared `LegalDocVersions` component, so the effective date lives in the TSX file; the "Last Modified" line is simply the first line of the MDX content file. Both were updated, each keeping its file's existing date format (`M D, YYYY` in the TSX, `D Month YYYY` in the MDX). No other content changed. ### Not changed — flagging for confirmation The same June 1, 2026 date appears on a few other surfaces. I left them alone because they are either different documents or point at a dated PDF asset that would need to be re-generated and re-uploaded. Let me know if any of these should move too: - `apps/www/pages/legal/dpa.tsx` — the legacy `/legal/dpa` page, which links `Supabase+DPA+260601.pdf` - `apps/studio/components/interfaces/Organization/Documents/DPA.tsx` — Studio links that same PDF - `apps/www/pages/legal/customer-resources/subprocessor-list.tsx` — the subprocessor list (`June-1-2026.pdf` and a "June 1, 2026" label); a separate document - `apps/www/pages/aup.mdx` — the Acceptable Use Policy, "Last Modified: 1 June 2026"; a separate document Co-authored-by: Claude --- .../legal/customer-resources/data-processing-addendum/v1.mdx | 2 +- .../pages/legal/customer-resources/data-processing-addendum.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx b/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx index 42c17b579df..a62a4bbb4dd 100644 --- a/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx +++ b/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx @@ -1,4 +1,4 @@ -_Last Modified: 1 June 2026_ +_Last Modified: 1 August 2026_ This Data Processing Addendum (the “**DPA**”) supplements and forms part of the Supabase Terms of Service available at [https://supabase.com/terms](https://supabase.com/terms), or such other agreement entered into between the Customer and Supabase Pte. Ltd (“**Supabase**”) relevant to Customer’s use of the Services (the “**Agreement**”), and in case of any conflict, supersedes the Agreement in relation to the transfer and processing of Covered Data in connection with the performance of the Services. This DPA is effective as of the Effective Date of the Agreement. diff --git a/apps/www/pages/legal/customer-resources/data-processing-addendum.tsx b/apps/www/pages/legal/customer-resources/data-processing-addendum.tsx index 8898d3603e2..0e4610d4b0d 100644 --- a/apps/www/pages/legal/customer-resources/data-processing-addendum.tsx +++ b/apps/www/pages/legal/customer-resources/data-processing-addendum.tsx @@ -14,7 +14,7 @@ const meta = { } const versions: LegalDocVersion[] = [ - { id: 'v1', label: 'Version 1', effectiveDate: 'June 1, 2026', Component: V1 }, + { id: 'v1', label: 'Version 1', effectiveDate: 'August 1, 2026', Component: V1 }, ] export default function DataProcessingAddendumPage() { From 44bc7b5a57cffa6f0041159063a173802a6735bf Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:49:30 +0200 Subject: [PATCH 119/141] feat(www): add Terms of Service v3 (effective August 1, 2026) (#48482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Requested by **Nicole Kramer** · [Slack thread](https://supabase.slack.com/archives/C0161K73J1J/p1785399344523739)_ ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Content update — adds a new version of the Terms of Service to the marketing site (`apps/www`). ## What is the current behavior? `/terms` offers two versions in the version dropdown: **Version 2 (May 6, 2026)**, shown by default, and **Version 1 (July 11, 2025)**. ## What is the new behavior? `/terms` shows **Version 3 (August 1, 2026)** by default. Version 2 and Version 1 are still selectable from the version dropdown (`/terms?version=v2`, `/terms?version=v1`) and are completely unchanged. Two files: - **New:** `apps/www/data/legal/terms/v3.mdx` — the full v3 Terms of Service. - **Changed:** `apps/www/pages/terms.tsx` — imports `V3` and prepends it to the `versions` array. Newest-first ordering is required, because `LegalDocVersions` treats `versions[0]` as the latest. ## Additional context ### The legal text is a verbatim transcription — please review it as such It comes from a Word document supplied by Legal, converted with pandoc and then verified character-exact against the source: **8055 words, 136 blocks, 0 word-level diffs**. Prettier was run over the file and changed nothing. **Known typos in the source document were deliberately preserved. Please do not correct them in review:** - §1 Definitions contains a **duplicate `d.`** — one `d.` introduces the Data Processing Addendum definition and the very next item is also lettered `d.` for the Documentation definition. The list therefore runs a, b, c, d, d, e … Re-lettering would shift internal cross-references, so it is left exactly as drafted. - §12(d) Survival contains a **doubled “and”** — *and Sections 1, 5, 6, 8, 9, 10, 11, and 13, and 14 survive*. Also preserved verbatim from the source: curly quotes on the Data Processing Addendum definition only (every other defined term uses straight quotes), and non-breaking spaces around the hyperlinks. ### Date The source document carries **no date line of its own**, even though its §14(d) states that the last-modified date *will be updated at the top of this Agreement*. August 1, 2026 was specified by the requester, and is placed in the `_Last Modified: 1 August 2026_` line at the top of the MDX, following the v1/v2 convention. As with v1 and v2, the `.mdx` uses `D Month YYYY` while `effectiveDate` in `terms.tsx` uses US long form (`August 1, 2026`). That split is pre-existing and intentional. ### New DPA link §1 of v3 links the Data Processing Addendum page at `/legal/customer-resources/data-processing-addendum`. This link is new relative to v2, and the DPA is also incorporated by reference in §7(b). ### No overlap with the concurrent DPA branch This PR touches only `apps/www/data/legal/terms/v3.mdx` and `apps/www/pages/terms.tsx` — **zero file overlap** with the branch updating the DPA page dates. ### Reviewer checklist - 14 numbered sections (1 Definitions → 14 Miscellaneous), 44 lettered subsections, 6 roman sub-subsections. - ALL-CAPS acceptance block at the top; ALL-CAPS AI disclaimer at the end of §9(b); ALL-CAPS §11 Limitations of Liability. - Three link targets intact: the DPA page, `https://supabase.com/privacy`, and `mailto:legal@supabase.io` (×3). Co-authored-by: Claude --- apps/www/data/legal/terms/v3.mdx | 273 +++++++++++++++++++++++++++++++ apps/www/pages/terms.tsx | 2 + 2 files changed, 275 insertions(+) create mode 100644 apps/www/data/legal/terms/v3.mdx diff --git a/apps/www/data/legal/terms/v3.mdx b/apps/www/data/legal/terms/v3.mdx new file mode 100644 index 00000000000..6ac34f24b22 --- /dev/null +++ b/apps/www/data/legal/terms/v3.mdx @@ -0,0 +1,273 @@ +_Last Modified: 1 August 2026_ + +These Terms of Service (this "Agreement") are a binding contract between you ("Customer," "you," or "your") and SUPABASE PTE. LTD., a Singapore entity with a registered address of 65 Chulia Street #38-02/03, OCBC Centre, Singapore 049513 ("Supabase," "we," or "us"). This Agreement governs your access to and use of the Cloud Services, except to the extent that Customer has entered into an Order or separate agreement with Supabase that expressly incorporates different terms, in which case those other terms will govern. Supabase and Customer may be referred to herein collectively as the "Parties" or individually as a "Party." + +THIS AGREEMENT TAKES EFFECT WHEN YOU CLICK THE "I ACCEPT" BUTTON BELOW OR BY ACCESSING OR USING THE SERVICES (the "Effective Date"). BY CLICKING ON THE "I ACCEPT" BUTTON BELOW OR BY ACCESSING OR USING THE SERVICES YOU (A) ACKNOWLEDGE THAT YOU HAVE READ AND UNDERSTAND THIS AGREEMENT; (B) REPRESENT AND WARRANT THAT YOU HAVE THE RIGHT, POWER, AND AUTHORITY TO ENTER INTO THIS AGREEMENT AND, IF ENTERING INTO THIS AGREEMENT FOR AN ORGANIZATION, THAT YOU HAVE THE LEGAL AUTHORITY TO BIND THAT ORGANIZATION; AND (C) ACCEPT THIS AGREEMENT AND AGREE THAT YOU ARE LEGALLY BOUND BY ITS TERMS. + +PLEASE READ THESE TERMS CAREFULLY TO ENSURE THAT YOU UNDERSTAND EACH PROVISION. THIS AGREEMENT CONTAINS A MANDATORY INDIVIDUAL ARBITRATION PROVISION IN SECTION 13(b) (THE "ARBITRATION AGREEMENT") AND A CLASS ACTION/JURY TRIAL WAIVER PROVISION IN SECTION 13(c) (THE "CLASS ACTION/JURY TRIAL WAIVER") THAT REQUIRE, UNLESS CUSTOMER OPTS OUT PURSUANT TO THE INSTRUCTIONS IN THE ARBITRATION AGREEMENT, THE EXCLUSIVE USE OF FINAL AND BINDING ARBITRATION ON AN INDIVIDUAL BASIS TO RESOLVE DISPUTES BETWEEN YOU AND US, INCLUDING ANY CLAIMS THAT AROSE OR WERE ASSERTED BEFORE YOU AGREED TO THIS AGREEMENT. TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW (AS DEFINED BELOW), YOU EXPRESSLY WAIVE YOUR RIGHT TO SEEK RELIEF IN A COURT OF LAW AND TO HAVE A JURY TRIAL ON YOUR CLAIMS, AS WELL AS YOUR RIGHT TO PARTICIPATE AS A PLAINTIFF OR CLASS MEMBER IN ANY CLASS, COLLECTIVE, PRIVATE ATTORNEY GENERAL, OR REPRESENTATIVE ACTION OR PROCEEDING. + +IF YOU DO NOT ACCEPT THESE TERMS, YOU MAY NOT ACCESS OR USE THE SERVICES. + +## 1. Definitions. + +a. "Aggregated Data" means data and information related to or derived from Customer Data or Customer's use of the Services that is used by Supabase in an aggregate and anonymized manner, including to compile statistical and performance information related to the Services. + +b. "Authorized User" means Customer's employees, consultants, contractors, and agents (i) who are authorized by Customer to access and use the Services under the rights granted to Customer pursuant to this Agreement; and (ii) for whom access to the Services has been purchased hereunder. + +c. "Customer Data" means information, data, and other content, in any form or medium, that is submitted, posted, or otherwise transmitted by or on behalf of Customer or an Authorized User through the Services; provided that, for purposes of clarity, Customer Data does not include Aggregated Data. + +d. “Data Processing Addendum” means the Data Processing Addendum available at [https://supabase.com/legal/customer-resources/data-processing-addendum](https://supabase.com/legal/customer-resources/data-processing-addendum), or, if the Parties have a separately executed agreement in effect that covers the same subject matter, the separately executed agreement. + +d. "Documentation" means Supabase's end user documentation relating to the Services available at supabase.io. + +e. "Harmful Code" means any software, hardware, or other technology, device, or means, including any virus, worm, malware, or other malicious computer code, the purpose or effect of which is to permit unauthorized access to, or to destroy, disrupt, disable, distort, or otherwise harm or impede in any manner any (i) computer, software, firmware, hardware, system, or network; or (ii) any application or function of any of the foregoing or the security, integrity, confidentiality, or use of any data processed thereby. + +f. "Order" means: (i) the purchase order, order form, or other ordering document entered into by the Parties that incorporates this Agreement by reference; or (ii) if Customer registered for the Services through Supabase's online ordering process, the results of such online ordering process. + +g. "Personal Information" means any information that, individually or in combination, does or can identify a specific individual or by or from which a specific individual may be identified, contacted, or located, including without limitation all data considered "personal data", "personally identifiable information", or something similar under applicable laws, rules, or regulations relating to data privacy. + +h. "Services" means Supabase's proprietary hosted software platform, as made available by Supabase to Authorized Users from time to time. + +i. "Supabase IP" means the Services, the Documentation, and any and all intellectual property provided to Customer or any Authorized User in connection with the foregoing. For the avoidance of doubt, Supabase IP includes Aggregated Data and any information, data, or other content derived from Supabase's provision of the Services but does not include Customer Data. + +j. "Third-Party Products" means any third-party products provided with, integrated with, or incorporated into the Services. + +k. "Usage Limitations" means the usage limitations set forth in this Agreement and the Order, including without limitation any limitations on the number of Authorized Users (if any), and the applicable product, pricing, and support tiers agreed-upon by the Parties. + +## 2. Access and Use. + +### a. Provision of Access + +Subject to and conditioned on Customer's compliance with the terms and conditions of this Agreement, including without limitation the Usage Limitations, Supabase will make available to Customer during the Subscription Period, on a non-exclusive, non-transferable (except in compliance with Section 14(g)), and non-sublicensable basis, access to and use of the Services, solely for use by Authorized Users. Such use is limited to Customer's internal business purposes and the features and functionalities specified in the Order. Supabase shall provide to Customer the necessary access credentials to allow Customer to access the Services. + +### b. Documentation License + +Subject to and conditioned on Customer's compliance with the terms and conditions of this Agreement, Supabase hereby grants to Customer a non-exclusive, non-transferable (except in compliance with Section 14(g)), and non-sublicensable license to use the Documentation during the Subscription Period solely for Customer's internal business purposes in connection with its use of the Services. + +### c. Use Restrictions + +Customer shall not use the Services for any purposes beyond the scope of the access granted in this Agreement. Customer shall not at any time, directly or indirectly, and shall not permit any Authorized Users to: (i) copy, modify, or create derivative works of any Supabase IP, whether in whole or in part; (ii) rent, lease, lend, sell, license, sublicense, assign, distribute, publish, transfer, or otherwise make available the Services or Documentation to any third party; (iii) reverse engineer, disassemble, decompile, decode, adapt, or otherwise attempt to derive or gain access to any software component of the Services, in whole or in part; (iv) remove any proprietary notices from any Supabase IP; (v) use any Supabase IP in any manner or for any purpose that infringes, misappropriates, or otherwise violates any intellectual property right or other right of any person, or that violates any applicable law; (vi) access or use any Supabase IP for purposes of competitive analysis of Supabase or the Services, the development, provision, or use of a competing software service or product, or any other purpose that is to Supabase's detriment or commercial disadvantage; (vii) bypass or breach any security device or protection used by the Services or access or use the Services other than by an Authorized User through the use of valid access credentials; or (viii) input, upload, transmit, or otherwise provide to or through the Services any information or materials that are unlawful or injurious, or that contain, transmit, or activate any Harmful Code. + +### d. Reservation of Rights + +Supabase reserves all rights not expressly granted to Customer in this Agreement. Except for the limited rights and licenses expressly granted under this Agreement, nothing in this Agreement grants, by implication, waiver, estoppel, or otherwise, to Customer or any third party, any intellectual property rights or other right, title, or interest in or to the Supabase IP. + +### e. Suspension + +Notwithstanding anything to the contrary in this Agreement, Supabase may temporarily suspend Customer's and any Authorized User's access to any portion or all of the Services if: (i) Supabase reasonably determines that (A) there is a threat or attack on any of the Supabase IP; (B) Customer's or any Authorized User's use of the Supabase IP disrupts or poses a security risk to the Supabase IP or to any other customer or vendor of Supabase; (C) Customer, or any Authorized User, is using the Supabase IP for fraudulent or illegal activities; (D) subject to applicable law, Customer has ceased to continue its business in the ordinary course, made an assignment for the benefit of creditors or similar disposition of its assets, or become the subject of any bankruptcy, reorganization, liquidation, dissolution, or similar proceeding; or (E) Supabase's provision of the Services to Customer or any Authorized User is prohibited by applicable law; (ii) any vendor of Supabase has suspended or terminated Supabase's access to or use of any Third-Party Products required to enable Customer to access the Services; or (iii) in accordance with Section 5(a) (any such suspension described in subclause (i), (ii), or (iii), a "Service Suspension"). Supabase shall use commercially reasonable efforts to provide written notice of any Service Suspension to Customer and to provide updates regarding resumption of access to the Services following any Service Suspension. Supabase shall use commercially reasonable efforts to resume providing access to the Services as soon as reasonably possible after the event giving rise to the Service Suspension is cured. Supabase will have no liability for any damage, liabilities, losses (including any loss of data or profits), or any other consequences that Customer or any Authorized User may incur as a result of a Service Suspension. + +### f. Aggregated Statistics + +Notwithstanding anything to the contrary in this Agreement, Supabase may monitor Customer's use of the Services and collect and compile Aggregated Data. As between Supabase and Customer, all right, title, and interest in Aggregated Data, and all intellectual property rights therein, belong to and are retained solely by Supabase. Customer acknowledges that Supabase may compile Aggregated Data based on Customer Data input into the Services. Customer agrees that Supabase may (i) make Aggregated Data available to third parties including its other customers in compliance with applicable law, and (ii) use Aggregated Data to the extent and in the manner permitted under applicable law. + +## 3. Customer Responsibilities. + +### a. General + +Customer is responsible and liable for all uses of the Services and Documentation resulting from access provided by Customer, directly or indirectly, whether such access or use is permitted by or in violation of this Agreement. Without limiting the generality of the foregoing, Customer is responsible for all acts and omissions of Authorized Users, and any act or omission by an Authorized User that would constitute a breach of this Agreement if taken by Customer will be deemed a breach of this Agreement by Customer. Customer shall use reasonable efforts to make all Authorized Users aware of this Agreement's provisions as applicable to such Authorized User's use of the Services and shall cause Authorized Users to comply with such provisions. + +### b. Third-Party Products + +Supabase may from time to time make Third-Party Products available to Customer or Supabase may allow for certain Third-Party Products to be integrated with the Services to allow for the transmission of Customer Data from such Third-Party Products into the Services. For purposes of this Agreement, such Third-Party Products are subject to their own terms and conditions. If Customer does not agree to abide by the applicable terms for any such Third-Party Products, then Customer should not install or use such Third-Party Products. By authorizing Supabase to transmit Customer Data from Third-Party Products into the Services, Customer represents and warrants to Supabase that it has all right, power, and authority to provide such authorization. + +### c. Customer Control and Responsibility + +Customer has and will retain sole responsibility for: (i) all Customer Data, including its content and use; (ii) all information, instructions, and materials provided by or on behalf of Customer or any Authorized User in connection with the Services; (iii) Customer's information technology infrastructure, including computers, software, databases, electronic systems (including database management systems), and networks, whether operated directly by Customer or through the use of third-party services ("Customer Systems"); (iv) the security and use of Customer's and its Authorized Users' access credentials; and (v) all access to and use of the Services directly or indirectly by or through the Customer Systems or its or its Authorized Users' access credentials, with or without Customer's knowledge or consent, including all results obtained from, and all conclusions, decisions, and actions based on, such access or use. + +### d. Responsible Use of AI-Powered Tools + +From time to time, Supabase may make available chatbots or other artificial intelligence-enabled tools to assist with customer support inquiries or other interactions (each, an "AI Tool"). Customer is responsible for the responsible and informed use of any AI Tool, including by: (a) ensuring that any data, content, prompts, or other materials Customer or its Authorized Users submit to an AI Tool (collectively, "AI Input") (i) are submitted with all necessary rights, consents, and authorizations, including with respect to any third-party or personal information contained therein, and (ii) do not violate applicable law, infringe or misappropriate any third-party right, or breach any obligation of confidentiality owed to a third party; (b) applying human judgment to review, evaluate, and verify any output generated by an AI Tool ("AI Output") before relying on it, using it or otherwise acting on it; (c) not treating any AI Output as legal, financial, security, or other professional advice; and (d) escalating to a human Supabase representative through Supabase's standard support channels whenever Customer requires authoritative confirmation, has a time-sensitive issue, or is otherwise not satisfied with the AI Output. + +## 4. Support. + +During the Subscription Period, Supabase will use commercially reasonable efforts to provide Customer with basic customer support via Supabase's standard support channels during Supabase's normal business hours. + +## 5. Fees and Payment. + +### a. Fees + +Where paid for services are agreed between Supabase and Customer per the Order, Customer shall pay Supabase the fees ("Fees") identified in the Order without offset or deduction at the cadence identified in the Order (e.g., monthly or annually). Fees paid by Customer are non-refundable. If Customer fails to make any payment when due, and Customer has not notified Supabase in writing within ten (10) days of the payment becoming due and payable that the payment is subject to a good faith dispute, without limiting Supabase's other rights and remedies: (i) Supabase may charge interest on the undisputed past due amount at the rate of 1.5% per month, calculated daily and compounded monthly or, if lower, the highest rate permitted under applicable law; (ii) Customer shall reimburse Supabase for all reasonable costs incurred by Supabase in collecting any late payments or interest, including attorneys' fees, court costs, and collection agency fees; and (iii) if such failure continues for ten (10) days or more, Supabase may suspend Customer's and its Authorized Users' access to any portion or all of the Services until such amounts are paid in full. + +#### i. Waiver of Withdrawal Right for EU and UK Consumers + +If you are a resident of the European Union or the United Kingdom and subject to applicable consumer protection laws, you acknowledge that by creating a project or initiating use of the Supabase services during the 14-day statutory withdrawal period, the services will begin immediately at your request. Accordingly, you expressly consent to the immediate provision of the services and acknowledge that you waive your right to cancel the service and receive a refund under applicable consumer protection laws. + +### b. Taxes + +All Fees and other amounts payable by Customer under this Agreement are exclusive of taxes and similar assessments. Customer is responsible for all sales, use, and excise taxes, and any other similar taxes, duties, and charges of any kind imposed by any federal, state, or local governmental or regulatory authority on any amounts payable by Customer hereunder, other than any taxes imposed on Supabase's income. To the extent that Supabase is required by law to pay any such taxes, duties, or other charges to any governmental or regulatory authority, Supabase may invoice Customer for such taxes, duties, or other charges and Customer will pay such invoiced amounts in accordance with this Agreement. + +### c. Credit Authorization and Fraud Prevention. + +To mitigate billing fraud and unauthorized usage, Supabase reserves the right to implement reasonable credit authorization, payment validation, and usage control measures, including but not limited to: (i) preauthorization or validation of Customer's payment method upon account creation or prior to provisioning Services; (ii) applying initial or ongoing spend limits, usage caps, or throttling mechanisms to accounts with insufficient billing history or elevated risk profiles; (iii) suspending or restricting access to Services where Supabase reasonably suspects fraudulent activity, failed payment authorization, or violation of usage terms; (iv) delaying the provisioning of high-cost Services or infrastructure pending verification or authorization; and (v) employing fraud-detection tools and analytics, including third-party services, to assess and manage risk. Supabase will make commercially reasonable efforts to notify Customer prior to suspending or restricting Service access under this Section, except in cases of suspected fraud, payment failure, or material risk to Supabase's infrastructure or systems. Customer agrees to cooperate with any reasonable verification or remediation steps required by Supabase to restore access. + +## 6. Confidential Information. + +### a. Definition + +From time to time during the Subscription Period, either Party may disclose or make available to the other Party information about its business affairs, products, confidential intellectual property, trade secrets, third-party confidential information, and other sensitive or proprietary information, whether orally or in written, electronic, or other form or media that: (i) is marked, designated or otherwise identified as "confidential" or something similar at the time of disclosure or within a reasonable period of time thereafter; or (ii) would be considered confidential by a reasonable person given the nature of the information or the circumstances of its disclosure (collectively, "Confidential Information"). Except for Personal Information, Confidential Information does not include information that, at the time of disclosure is: (a) in the public domain; (b) known to the receiving Party at the time of disclosure; (c) rightfully obtained by the receiving Party on a non-confidential basis from a third party; or (d) independently developed by the receiving Party without use of, reference to, or reliance upon the disclosing Party's Confidential Information. + +### b. Duty + +The receiving Party shall not disclose the disclosing Party's Confidential Information to any person or entity, except to the receiving Party's employees, contractors, and agents who have a need to know the Confidential Information for the receiving Party to exercise its rights or perform its obligations hereunder ("Representatives"). The receiving Party will be responsible for all the acts and omissions of its Representatives as they relate to Confidential Information hereunder. Notwithstanding the foregoing, each Party may disclose Confidential Information to the limited extent required (i) in order to comply with the order of a court or other governmental body, or as otherwise necessary to comply with applicable law, provided that the Party making the disclosure pursuant to the order shall first have given written notice to the other Party and made a reasonable effort to obtain a protective order; or (ii) to establish a Party's rights under this Agreement, including to make required court filings. Further, notwithstanding the foregoing, each Party may disclose the terms and existence of this Agreement to its actual or potential investors, debtholders, acquirers, or merger partners under customary confidentiality terms. + +### c. Return of Materials; Effect of Termination/Expiration + +On the expiration or termination of the Agreement, the receiving Party shall promptly return to the disclosing Party all copies, whether in written, electronic, or other form or media, of the disclosing Party's Confidential Information, or destroy all such copies and certify in writing to the disclosing Party that such Confidential Information has been destroyed. Each Party's obligations of non-use and non-disclosure with regard to Confidential Information are effective as of the Effective Date and will expire three (3) years from the date of termination or expiration of this Agreement; provided, however, with respect to any Confidential Information that constitutes a trade secret (as determined under applicable law), such obligations of non-disclosure will survive the termination or expiration of this Agreement for as long as such Confidential Information remains subject to trade secret protection under applicable laws and regulations. + +## 7. Personal Information. + +### a. Account Information. + +Supabase's privacy policy, available at [https://supabase.com/privacy](https://supabase.com/privacy) ("Privacy Policy") is subject to change as described therein. By accessing, using, and providing information to or through the Services, Customer acknowledges that it has reviewed the Privacy Policy, and consents to all actions taken by Supabase with respect to Customer's information in compliance with the then-current version of the Privacy Policy. + +### b. Customer Data. + +Customer will ensure that its Customer Data, and its use of such Customer Data, complies with this Agreement and any applicable law. Customer is responsible for properly configuring and using the Services and taking its own steps to maintain appropriate security, protection, and backup of Customer Data. Customer may not store or process protected health information (as defined in HIPAA) using the Services unless Customer signs a Business Associate Agreement with Supabase. Customer may not store any payment cardholder information using the Services without Supabase's prior written approval. The Parties agree to comply with the Data Processing Addendum, which is incorporated into this Agreement. + +## 8. Intellectual Property Ownership; Feedback. + +### a. Supabase IP + +Customer acknowledges that, as between Customer and Supabase, Supabase owns all right, title, and interest, including all intellectual property rights, in and to the Supabase IP and, with respect to Third-Party Products, the applicable third-party providers own all right, title, and interest, including all intellectual property rights, in and to the Third-Party Products. + +### b. Customer Data + +Supabase acknowledges that, as between Supabase and Customer, Customer owns all right, title, and interest, including all intellectual property rights, in and to the Customer Data. Customer hereby grants to Supabase a non-exclusive, royalty-free, worldwide license to reproduce, distribute, and otherwise use and display the Customer Data and perform all acts with respect to the Customer Data as may be necessary for Supabase to provide the Services to Customer, and a non-exclusive, perpetual, irrevocable, royalty-free, worldwide license to reproduce, distribute, modify, and otherwise use and display Customer Data incorporated within the Aggregated Data. Customer may export the Customer Data at any time through the features and functionalities made available via the Services. To the extent Supabase utilizes artificial intelligence to provide features to Customer via the Services, and in Supabase's provision of the AI Tools, Supabase will not use, nor allow any third-party to use, Customer Data, Customer's AI Input, or any AI Output to train, fine-tune, or otherwise improve any artificial intelligence or machine learning model, without Customer's prior written consent. For the avoidance of doubt, Customer retains all right, title and interest in Customer Data contained in AI Inputs and AI Outputs. + +### c. Feedback + +If Customer or any of its employees or contractors sends or transmits any communications or materials to Supabase by mail, email, telephone, or otherwise, suggesting or recommending changes to the Supabase IP, including without limitation, new features or functionality relating thereto, or any comments, questions, suggestions, or the like ("Feedback"), Supabase is free to use such Feedback irrespective of any other obligation or limitation between the Parties governing such Feedback so long as Supabase does not identify Customer as the source of the Feedback without Customer's prior approval. + +## 9. Warranties; Disclaimer + +### a. Customer + +Customer represents, warrants, and covenants to Supabase that Customer owns or otherwise has and will have the necessary rights and consents in and relating to the Customer Data so that, as received by Supabase and processed in accordance with this Agreement, they do not and will not infringe, misappropriate, or otherwise violate any intellectual property, privacy or other rights of any third party or violate any applicable laws or regulations. + +### b. Disclaimer + +Notwithstanding anything to the contrary, the Supabase IP is provided "as is" and Supabase hereby disclaims all warranties, whether express, implied, statutory, or otherwise. Supabase specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, title, and non-infringement, and all warranties arising from course of dealing, usage, or trade practice. Supabase makes no warranty of any kind that the Supabase IP, or any products or results of the use thereof, will meet Customer's or any other person's requirements, operate without interruption, achieve any intended result, be compatible or work with any software, system or other services, or be secure, accurate, complete, free of harmful code, or error free. In addition, Customer acknowledges that AI Tools are powered by one or more third-party large language models that Supabase does not own or control, and that AI Outputs may be inaccurate, incomplete, or unsuitable for Customer's particular use case. Customer acknowledges that other customers providing similar AI Input may receive similar or the same AI Output. AI TOOLS AND THEIR OUTPUTS ARE PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS, AND SUPABASE MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, AS TO THE ACCURACY, RELIABILITY, COMPLETENESS, ERROR-FREE NATURE, NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE OF ANY OUTPUT GENERATED FROM THE AI TOOLS. + +## 10. Indemnification + +### a. Supabase Indemnification + +Supabase shall indemnify, defend, and hold harmless Customer from and against any and all losses, damages, liabilities, costs (including reasonable attorneys' fees) ("Losses") incurred by Customer resulting from any third-party claim, suit, action, or proceeding ("Third-Party Claim") that the Services, or any use of the Services in accordance with this Agreement, infringes or misappropriates such third party's US copyrights or trade secrets; provided that Customer promptly notifies Supabase in writing of the claim, cooperates with Supabase, and allows Supabase sole authority to control the defense and settlement of such claim. + +If such a claim is made or appears possible, Customer agrees to permit Supabase, at Supabase's sole discretion: to (i) modify or replace the Services, or component or part thereof, to make it non-infringing; or (ii) obtain the right for Customer to continue use. If Supabase determines that neither alternative is reasonably commercially available, Supabase may terminate this Agreement, in its entirety or with respect to the affected component or part, effective immediately on written notice to Customer. + +This Section 10(a) will not apply to the extent that the alleged infringement arises from: (i) use of the Services in combination with data, software, hardware, equipment, or technology not provided by Supabase or authorized by Supabase in writing; (ii) modifications to the Services not made by Supabase; (iii) Customer Data; (iv) Third-Party Products; (v) AI Output; or (vi) AI Input. + +### b. Customer Indemnification + +Customer shall indemnify, hold harmless, and, at Supabase's option, defend Supabase from and against any Losses resulting from any Third-Party Claim that the Customer Data, AI Input or any use of the Customer Data in accordance with this Agreement, infringes or misappropriates such third party's US intellectual property or other rights and any Third-Party Claims based on Customer's or any Authorized User's (i) negligence or willful misconduct; (ii) use of the Services in a manner not authorized by this Agreement; or (iii) use of the Services in combination with data, software, hardware, equipment or technology not provided by Supabase or authorized by Supabase in writing; in each case provided that Customer may not settle any Third-Party Claim against Supabase unless Supabase consents to such settlement, and further provided that Supabase will have the right, at its option, to defend itself against any such Third-Party Claim or to participate in the defense thereof by counsel of its own choice. + +### c. Sole Remedy + +This Section 10 sets forth Customer's sole remedies and Supabase's sole liability and obligation for any actual, threatened, or alleged claims that the services infringe, misappropriate, or otherwise violate any intellectual property rights of any third party. + +## 11. Limitations of Liability. + +EXCEPT FOR: (I) A PARTY'S BREACH OF ITS CONFIDENTIALITY OBLIGATIONS; (II) A PARTY'S INDEMNITY OBLIGATIONS; OR (III) A PARTY'S GROSS NEGLIGENCE, FRAUD, OR WILLFUL MISCONDUCT ("EXCLUDED LIABILITIES"), (A) IN NO EVENT WILL EITHER PARTY BE LIABLE UNDER OR IN CONNECTION WITH THIS AGREEMENT UNDER ANY LEGAL OR EQUITABLE THEORY, INCLUDING BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, AND OTHERWISE, FOR ANY: (1) CONSEQUENTIAL, INCIDENTAL, INDIRECT, EXEMPLARY, SPECIAL, ENHANCED, OR PUNITIVE DAMAGES; (2) INCREASED COSTS, DIMINUTION IN VALUE OR LOST BUSINESS, PRODUCTION, REVENUES, OR PROFITS; (3) LOSS OF GOODWILL OR REPUTATION; (4) USE, INABILITY TO USE, LOSS, INTERRUPTION, DELAY OR RECOVERY OF ANY DATA, OR BREACH OF DATA OR SYSTEM SECURITY; OR (5) COST OF REPLACEMENT GOODS OR SERVICES, IN EACH CASE REGARDLESS OF WHETHER SUCH PARTY WAS ADVISED OF THE POSSIBILITY OF SUCH LOSSES OR DAMAGES OR SUCH LOSSES OR DAMAGES WERE OTHERWISE FORESEEABLE; AND (B) IN NO EVENT WILL EITHER PARTY'S AGGREGATE LIABILITY ARISING OUT OF OR RELATED TO THIS AGREEMENT UNDER ANY LEGAL OR EQUITABLE THEORY, INCLUDING BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, AND OTHERWISE EXCEED THE TOTAL AMOUNTS PAID AND/OR PAYABLE TO SUPABASE UNDER THIS AGREEMENT IN THE TWELVE (12) MONTHS IMMEDIATELY PRECEDING THE CLAIM; PROVIDED THAT, NOTWITHSTANDING THE FOREGOING, SUPABASE'S AGGREGATE LIABILITY ARISING OUT OF OR RELATING TO ANY EXCLUDED LIABILITIES WILL NOT EXCEED THREE TIMES (3X) THE TOTAL AMOUNTS PAID AND/OR PAYABLE TO SUPABASE BY CUSTOMER UNDER THIS AGREEMENT IN THE TWELVE (12) MONTHS IMMEDIATELY PRECEDING THE CLAIM. + +## 12. Subscription Period and Termination. + +### a. Subscription Period + +The initial term of this Agreement begins on the Effective Date and, unless terminated earlier pursuant to Section 12(b), will continue in effect for the period identified in the Order (the "Initial Subscription Period"). This Agreement will automatically renew for additional successive terms equal to the length of the Initial Subscription Period unless earlier terminated pursuant to this Agreement's express provisions or either Party gives the other Party written notice of non-renewal at least thirty (30) days prior to the expiration of the then-current term (each a "Renewal Subscription Period" and together with the Initial Subscription Period, the "Subscription Period"). + +### b. Termination + +In addition to any other express termination right set forth in this Agreement: + +Supabase may terminate this Agreement, effective on written notice to Customer, if Customer: (i) fails to pay any amount when due hereunder, and such failure continues more than ten (10) calendar days after Supabase's delivery of written notice thereof; or (ii) breaches any of its obligations under Section 2(c) or Section 6. + +Either Party may terminate this Agreement, effective on written notice to the other Party, if the other Party materially breaches this Agreement, and such breach: (i) is incapable of cure; or (ii) being capable of cure, remains uncured thirty (30) calendar days after the non-breaching Party provides the breaching Party with written notice of such breach. + +Either Party may terminate this Agreement, effective immediately upon written notice to the other Party, if the other Party: (i) becomes insolvent or is generally unable to pay, or fails to pay, its debts as they become due; (ii) files or has filed against it, a petition for voluntary or involuntary bankruptcy or otherwise becomes subject, voluntarily or involuntarily, to any proceeding under any domestic or foreign bankruptcy or insolvency law; (iii) makes or seeks to make a general assignment for the benefit of its creditors; or (iv) applies for or has appointed a receiver, trustee, custodian, or similar agent appointed by order of any court of competent jurisdiction to take charge of or sell any material portion of its property or business. + +### c. Effect of Expiration or Termination + +Upon expiration or earlier termination of this Agreement, Customer shall immediately discontinue use of the Supabase IP and, without limiting Customer's obligations under Section 6, Customer shall delete, destroy, or return all copies of the Supabase IP and certify in writing to the Supabase that the Supabase IP has been deleted or destroyed. No expiration or termination will affect Customer's obligation to pay all Fees that may have become due before such expiration or termination or entitle Customer to any refund. + +### d. Survival + +This Section 12(d), and Sections 1, 5, 6, 8, 9, 10, 11, and 13, and 14 survive any termination or expiration of this Agreement. No other provisions of this Agreement survive the expiration or earlier termination of this Agreement. + +## 13. Governing Law, Arbitration and Class Action/Jury Waiver + +### a. Governing Law + +Customer agrees that: (a) the Services will be deemed solely based in the State of California; and (b) the Service will be deemed a passive one that does not give rise to personal jurisdiction over us, either specific or general, in jurisdictions other than California. This Agreement will be governed by the internal substantive laws of the State of California, without respect to its conflict of laws principles. The parties acknowledge that this Agreement evidences a transaction involving interstate commerce. Notwithstanding the preceding sentences with respect to the substantive law governing this Agreement, the Federal Arbitration Act (9 U.S.C. §§ 1-16) (as it may be amended, "FAA") governs the interpretation and enforcement of the Arbitration Agreement below and preempts all state laws (and laws of other jurisdictions) to the fullest extent permitted by applicable laws and regulations. If the FAA is found to not apply to any issue that arises from or relates to the Arbitration Agreement, then that issue will be resolved under and governed by the law of the U.S. state where Customer resides (if applicable) or the jurisdiction mutually agreed upon in writing by the Parties. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Customer agrees to submit to the exclusive personal jurisdiction of the federal and state courts located in California for any actions for which Supabase retains the right to seek injunctive or other equitable relief in a court of competent jurisdiction to prevent the actual or threatened infringement, misappropriation, or violation of data security, Confidential Information, or intellectual property rights, as set forth in the Arbitration Agreement below, including any provisional relief required to prevent irreparable harm. Customer agrees that California is the proper and exclusive forum for any appeals of an arbitration award, or for trial court proceedings in the event that the Arbitration Agreement below is found to be unenforceable. This Agreement was drafted in the English language and this English language version of the Agreement is the original, governing instrument of the understanding between the Parties. In the event of any conflict between the English version of this Agreement and any translation, the English version will prevail. + +### b. Arbitration Agreement + +#### i. General + +READ THIS SECTION CAREFULLY BECAUSE IT REQUIRES THE PARTIES TO ARBITRATE THEIR DISPUTES AND LIMITS THE MANNER IN WHICH CUSTOMER CAN SEEK RELIEF FROM SUPABASE. This Arbitration Agreement applies to and governs any dispute, controversy, or claim between the Parties that arises out of or relates to, directly or indirectly: (i) this Agreement, including the formation, existence, breach, termination, enforcement, interpretation, validity, and enforceability thereof; (ii) access to or use of the Services, including receipt of any advertising or marketing communications; (iii) any transactions through, by, or using the Services; or (iv) any other aspect of Customer's relationship or transactions with Supabase, directly or indirectly, as a user or consumer (each, a "Claim," and, collectively, "Claims"). This Arbitration Agreement will apply, without limitation, to all Claims that arose or were asserted before or after Customer's consent to this Agreement. + +#### ii. Opting Out of Arbitration Agreement + +If you are a new customer, you can reject and opt out of this Arbitration Agreement within thirty (30) days of accepting this Agreement by emailing Supabase at [legal@supabase.io](mailto:legal@supabase.io) with your full, legal name (or the name of the organization that you had the legal authority to bind to this Agreement if you entered this Agreement for an organization) and stating your intent to opt out of this Arbitration Agreement. Opting out of this Arbitration Agreement does not affect the binding nature of any other part of this Agreement, including the provisions regarding controlling law or the courts in which any disputes must be brought. + +#### iii. Dispute-Resolution Process + +For any Claim, Customer will first contact Supabase at [legal@supabase.io](mailto:legal@supabase.io) and attempt to resolve the Claim with Supabase informally. In the unlikely event that the Parties have not been able to resolve a Claim after sixty (60) days, the Claim shall be finally settled under the Rules of Arbitration ("Rules") of the International Chamber of Commerce ("ICC") by one or more arbitrators (each, an "Arbitrator") appointed in accordance with such Rules. The place of arbitration shall be: (a) Singapore to the extent that Customer is located in Asia; (b) London, United Kingdom to the extent that Customer is located in Europe; or (c) San Francisco County, California to the extent that Customer is located in the United States or any other jurisdiction, in each case unless the Parties agree otherwise. If Customer is using the Service for commercial purposes, each party will be responsible for paying any ICC filing and administrative fees and Arbitrator fees in accordance with the Rules, and the award rendered by the Arbitrator will include costs of arbitration, reasonable attorneys' fees, and reasonable costs for expert and other witnesses. If Customer is an individual using the Services for non-commercial purposes: (i) ICC may require Customer to pay a fee for the initiation of a case; (ii) the award rendered by the Arbitrators may include Customer's costs of arbitration, reasonable attorneys' fees, and reasonable costs for expert and other witnesses; and (iii) Customer may sue in a small claims court of competent jurisdiction without first engaging in arbitration, but this would not absolve Customer of any commitment to engage in the informal dispute resolution process. Any judgment on the award rendered by the Arbitrators may be entered in any court of competent jurisdiction. The Parties agree that the Arbitrators, and not any federal, state, or local court or agency, will have exclusive authority to resolve any disputes relating to the scope, interpretation, applicability, enforceability, or formation of this Arbitration Agreement, including any claim that all or any part of this Arbitration Agreement is void or voidable. The Arbitrator will also be responsible for determining all threshold arbitrability issues, including issues relating to whether this Agreement is, or whether any provision hereof, unconscionable or illusory, and any defense to arbitration, including waiver, delay, laches, unconscionability, and/or estoppel. + +#### iv. Equitable Relief + +Nothing in this Arbitration Agreement will be deemed as: preventing Supabase from seeking injunctive or other equitable relief from the courts as necessary to prevent the actual or threatened infringement, misappropriation, or violation of Supabase's data security, confidential information, or intellectual property rights; or preventing Customer from asserting claims in a small claims court, provided that Customer's claims qualify and so long as the matter remains in such court and advances on only an individual (non-class, non-collective, and non-representative) basis. + +#### v. Severability + +If this Arbitration Agreement is found to be void, unenforceable, or unlawful, in whole or in part, the void, unenforceable, or unlawful provision, in whole or in part, will be severed. Severance of the void, unenforceable, or unlawful provision, in whole or in part, will have no impact on the remaining provisions of this Arbitration Agreement, which will remain in force, or on the Parties' ability to compel arbitration of any remaining Claims on an individual basis pursuant to this Arbitration Agreement. Notwithstanding the foregoing, if the Class Action/Jury Trial Waiver below is found to be void, unenforceable, or unlawful, in whole or in part, because it would prevent Customer from seeking public injunctive relief, then any dispute regarding the entitlement to such relief (and only that relief) must be severed from arbitration and may be litigated in a civil court of competent jurisdiction. All other claims for relief subject to arbitration under this Arbitration Agreement will be arbitrated under its terms, and the Parties agree that litigation of any dispute regarding the entitlement to public injunctive relief will be stayed pending the outcome of any individual claims in arbitration. + +### c. Class Action/Jury Trial Waiver + +By entering into this Agreement, each Party is waiving the right to a trial by jury or to bring, join, or participate in any purported class action, collective action, private attorney general action, or other representative proceeding of any kind as a plaintiff or class member. The foregoing applies to all users (both natural persons and entities), regardless of whether Customer has obtained or used the service for personal, commercial, or other purposes. This class action/jury trial waiver applies to class arbitration, and, unless the Parties agree otherwise, the Arbitrators may not consolidate more than one person's or entity's claims. The Parties agree that the Arbitrators may award relief only to an individual claimant and only to the extent necessary to provide relief on Customer's individual claim(s). Any relief awarded may not affect other users. + +## 14. Miscellaneous. + +### a. Entire Agreement + +This Agreement, together with any other documents incorporated herein by reference, constitutes the sole and entire agreement of the Parties with respect to the subject matter of this Agreement and supersedes all prior and contemporaneous understandings, agreements, and representations and warranties, both written and oral, with respect to such subject matter. In the event of any inconsistency between the statements made in the body of this Agreement, the related Exhibits, and any other documents incorporated herein by reference, the following order of precedence governs: (i) first, this Agreement; and (ii) second, any other documents incorporated herein by reference. + +### b. Notices + +All notices, requests, consents, claims, demands, waivers, and other communications hereunder (each, a "Notice") must be in writing and addressed to the Parties at the addresses set forth on the first page of this Agreement or as identified on the Order Form (or to such other address that may be designated by the Party giving Notice from time to time in accordance with this Section). All Notices must be delivered by personal delivery, nationally recognized signed for on delivery courier (with all fees pre-paid), or email (with confirmation of transmission). All email Notices to Supabase must be sent to [legal@supabase.io](mailto:legal@supabase.io). Except as otherwise provided in this Agreement, a Notice is effective only: (i) upon receipt by the receiving Party; and (ii) if the Party giving the Notice has complied with the requirements of this Section 14(b). Notwithstanding the foregoing, Customer hereby consents to receiving electronic communications from Supabase, which may include notices about applicable fees and charges, transactional information, and other information concerning or related to the Services. Customer agrees that any notices, agreements, disclosures, or other communications that Supabase sends to Customer electronically will satisfy any legal communication requirements, including that such communications be in writing. + +### c. Force Majeure + +In no event shall either Party be liable to the other Party, or be deemed to have breached this Agreement, for any failure or delay in performing its obligations under this Agreement (except for any obligations to make payments), if and to the extent such failure or delay is caused by any circumstances beyond such Party's reasonable control, including but not limited to acts of God, flood, fire, earthquake, explosion, war, terrorism, invasion, riot or other civil unrest, strikes, labor stoppages or slowdowns or other industrial disturbances, or passage of law or any action taken by a governmental or public authority, including imposing an embargo. + +### d. Amendment and Modification + +Supabase may change this Agreement (except for any Orders) from time to time at its discretion. The date on which the Agreement was last modified will be updated at the top of this Agreement. Supabase will provide Customer with reasonable notice prior to any amendments or modifications taking effect, either by emailing the email address associated with Customer's account on the Services or by another method reasonably designed to provide notice to Customer. If Customer accesses or uses the Services after the effective date of the revised Agreement, such access and use will constitute Customer's acceptance of the revised Agreement beginning at the next Renewal Subscription Period or, if Customer enters into a new Order with Supabase, as of the date of execution of such Order. + +### e. Waiver + +No failure or delay by either Party in exercising any right or remedy available to it in connection with this Agreement will constitute a waiver of such right or remedy. No waiver under this Agreement will be effective unless made in writing and signed by an authorized representative of the Party granting the waiver. + +### f. Severability + +If any provision of this Agreement is invalid, illegal, or unenforceable in any jurisdiction, such invalidity, illegality, or unenforceability will not affect any other term or provision of this Agreement or invalidate or render unenforceable such term or provision in any other jurisdiction. Upon such determination that any term or other provision is invalid, illegal, or unenforceable, the Parties shall negotiate in good faith to modify this Agreement so as to effect their original intent as closely as possible in a mutually acceptable manner in order that the transactions contemplated hereby be consummated as originally contemplated to the greatest extent possible. + +### g. Assignment + +Customer may not assign any of its rights or delegate any of its obligations hereunder, in each case whether voluntarily, involuntarily, by operation of law or otherwise, without the prior written consent of Supabase. Any purported assignment or delegation in violation of this Section will be null and void. No assignment or delegation will relieve the assigning or delegating Party of any of its obligations hereunder. This Agreement is binding upon and inures to the benefit of the Parties and their respective permitted successors and assigns. + +### h. Export Regulation + +The Services utilize software and technology that may be subject to US export control laws, including the US Export Administration Act and its associated regulations. Customer shall not, directly or indirectly, export, re-export, or release the Services or the underlying software or technology to, or make the Services or the underlying software or technology accessible from, any jurisdiction or country to which export, re-export, or release is prohibited by law, rule, or regulation. Customer shall comply with all applicable federal laws, regulations, and rules, and complete all required undertakings (including obtaining any necessary export license or other governmental approval), prior to exporting, re-exporting, releasing, or otherwise making the Services or the underlying software or technology available outside the US. + +### i. US Government Rights + +Each of the Documentation and the software components that constitute the Services is a "commercial item" as that term is defined at 48 C.F.R. § 2.101, consisting of "commercial computer software" and "commercial computer software documentation" as such terms are used in 48 C.F.R. § 12.212. Accordingly, if Customer is an agency of the US Government or any contractor therefor, Customer only receives those rights with respect to the Services and Documentation as are granted to all other end users, in accordance with (a) 48 C.F.R. § 227.7201 through 48 C.F.R. § 227.7204, with respect to the Department of Defense and their contractors, or (b) 48 C.F.R. § 12.212, with respect to all other US Government users and their contractors. + +### j. Equitable Relief + +Each Party acknowledges and agrees that a breach or threatened breach by such Party of any of its obligations under Section 6 or, in the case of Customer, Section 2(c), would cause the other Party irreparable harm for which monetary damages would not be an adequate remedy and agrees that, in the event of such breach or threatened breach, the other Party will be entitled to equitable relief, including a restraining order, an injunction, specific performance and any other relief that may be available from any court, without any requirement to post a bond or other security, or to prove actual damages or that monetary damages are not an adequate remedy. Such remedies are not exclusive and are in addition to all other remedies that may be available at law, in equity or otherwise. + +### k. Publicity + +Supabase may identify Customer as a user of the Services and may use Customer's name, logo, and other trademarks in Supabase's customer list, press releases, blog posts, advertisements, and website (and all use thereof and goodwill arising therefrom shall inure to the sole and exclusive benefit of Customer). Otherwise, neither Party may use the name, logo, or other trademarks of the other Party for any purpose without the other Party's prior written approval. diff --git a/apps/www/pages/terms.tsx b/apps/www/pages/terms.tsx index 87e4f171f87..a1f603dd6bc 100644 --- a/apps/www/pages/terms.tsx +++ b/apps/www/pages/terms.tsx @@ -4,6 +4,7 @@ import SectionContainer from '~/components/Layouts/SectionContainer' import LegalDocVersions, { type LegalDocVersion } from '~/components/Legal/LegalDocVersions' import V1 from '~/data/legal/terms/v1.mdx' import V2 from '~/data/legal/terms/v2.mdx' +import V3 from '~/data/legal/terms/v3.mdx' import mdxComponents from '~/lib/mdx/mdxComponents' import { NextSeo } from 'next-seo' @@ -13,6 +14,7 @@ const meta = { } const versions: LegalDocVersion[] = [ + { id: 'v3', label: 'Version 3', effectiveDate: 'August 1, 2026', Component: V3 }, { id: 'v2', label: 'Version 2', effectiveDate: 'May 6, 2026', Component: V2 }, { id: 'v1', label: 'Version 1', effectiveDate: 'July 11, 2025', Component: V1 }, ] From 8a607a6108a110e2653b1b38853c09eb178d7437 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:49:40 +0200 Subject: [PATCH 120/141] feat(www): add Enterprise SaaS Subscription Agreement v3 (#48484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Requested by **Nicole Kramer** · [Slack thread](https://supabase.slack.com/archives/C0161K73J1J/p1785399555203219)_ ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Content update — a new version of a published legal agreement. ## What is the current behavior? The version selector on `/enterprise-terms` offers two versions of the Enterprise SaaS Subscription Agreement: Version 2 (May 6, 2026) and Version 1 (April 17, 2026). Version 2 is what the page shows by default. ## What is the new behavior? **Before:** opening `/enterprise-terms` showed Version 2 — May 6, 2026. **After:** it shows **Version 3 — August 1, 2026**. Versions 2 and 1 are unchanged and still reachable from the dropdown (`?version=v2`, `?version=v1`). Two files change: - **New** `apps/www/data/legal/enterprise-terms/v3.mdx` — the Version 3 text, transcribed from the source Word document supplied in the Slack thread (`2026.07.29 - Supabase - Enterprise Terms.docx`). - `apps/www/pages/enterprise-terms.tsx` — imports the new MDX and prepends `{ id: 'v3', label: 'Version 3', effectiveDate: 'August 1, 2026', Component: V3 }` to the `versions` array. Since the array is newest-first, v3 becomes the default. ## Additional context ### Transcription fidelity The legal text was not edited, reworded, reordered, or corrected — only re-rendered in the MDX conventions already used by `v1.mdx` and `v2.mdx`. This was verified mechanically rather than by eye: markdown markup was stripped from `v3.mdx` and the result diffed paragraph-by-paragraph against text extracted directly from the source document's OOXML. - **137 paragraphs in the source, 137 in `v3.mdx`, 0 differing.** - 14 top-level sections and 48 subsections, matching the source's heading counts exactly. - All 3 distinct URLs preserved, written as bare URLs per the existing convention in this file family (remark-gfm autolinks them). - Pure ASCII apart from 5 `§` characters in the 48 C.F.R. citations, matching `v1.mdx`/`v2.mdx`. - Prettier clean; no British spellings that would trip the US-locale misspell check. Formatting decisions worth knowing: the source document contains no bold or italic runs at all, but `v1.mdx` and `v2.mdx` both bold defined terms and section numbers, so v3 follows that house style for consistent rendering across the three versions. The source also carries no date or version line of its own; the `_Last Modified: 1 August 2026_` first line is repo convention, matching how every other legal MDX in `apps/www/data/legal/` is written. ### What changed from v2, in the legal text Structure is identical — same 14 sections, same 48 subsection titles. Five substantive prose changes: 1. **Preamble** — the effective date is now "the date of last signature of an Order referencing these terms", replacing v2's unfilled `[Deal.CloseDate]` merge-field placeholder. "Signature block below" becomes "signature block in an Order". 2. **New § 1.4 "Data Processing Addendum"** — defined by reference to `https://supabase.com/legal/customer-resources/data-processing-addendum`, with a carve-out for a separately executed agreement covering the same subject matter. Former § 1.4–1.12 shift to § 1.5–1.13; nothing was removed or reordered. 3. **§ 7.2 Data Processing** replaced — v2's GDPR / UK GDPR / Swiss clause is gone, replaced by a general compliance paragraph that incorporates the Data Processing Addendum into the Agreement. 4. **§ 13.3** cross-reference corrected from Section 10.1 (Mutual) to Section 10.3 (Limited Warranty), which is the clause the refund remedy actually depends on. 5. **§ 14.4 Amendment and Modification** rewritten — v2 required a writing executed by both Parties; v3 gives Supabase a unilateral right to modify by posting a revised version at `https://supabase.com/enterprise-terms`, effective the first day of the following calendar month, or at the start of the next Renewal Subscription Period for Orders with a fixed Subscription Period of 12 months or longer, with non-renewal under § 13.1 as Customer's sole and exclusive remedy. Two things carried over verbatim from the source rather than fixed, since the text must not be edited: § 7.2 is now near-duplicative of § 7.1 (three of its four sentences repeat § 7.1 almost word for word), and "HIPAA" is used in both § 7.1 and § 7.2 without being defined. One pre-existing inconsistency, unrelated to this change: `v2.mdx`'s own first line reads `_Last Modified: 1 May 2026_` while the page lists Version 2's effective date as `May 6, 2026`. Left alone here. ### Overlap with concurrent work Two sibling changes are in flight for the same requester today — one adjusting the new Data Processing Addendum page's dates, one publishing Terms of Service v3. Neither touches these two files, but all three touch the `apps/www/data/legal/` tree, and note that § 1.4 above now links to the DPA page. [#48483](https://github.com/supabase/supabase/pull/48483) removes the `_Last Modified:` first line from every versioned legal MDX, on the principle that the version selector should be the only place a date appears. It is sequenced to land after this PR, and its file list predates `v3.mdx`. A three-way merge of the two branches is clean, but whoever rebases #48483 should add `apps/www/data/legal/enterprise-terms/v3.mdx` to that removal — otherwise v3 keeps a body date after v1 and v2 lose theirs. The `_Last Modified: 1 May 2026` / `May 6, 2026` mismatch on v2 is also handled in #48483 and is deliberately left alone here, so the same line isn't touched by two PRs. Co-authored-by: Claude --- apps/www/data/legal/enterprise-terms/v3.mdx | 275 ++++++++++++++++++++ apps/www/pages/enterprise-terms.tsx | 2 + 2 files changed, 277 insertions(+) create mode 100644 apps/www/data/legal/enterprise-terms/v3.mdx diff --git a/apps/www/data/legal/enterprise-terms/v3.mdx b/apps/www/data/legal/enterprise-terms/v3.mdx new file mode 100644 index 00000000000..c156a7e28cb --- /dev/null +++ b/apps/www/data/legal/enterprise-terms/v3.mdx @@ -0,0 +1,275 @@ +_Last Modified: 1 August 2026_ + +This Enterprise SaaS Subscription Agreement (this "**Agreement**") is entered into by and between the customer identified in the signature block in an Order ("**Customer**") and Supabase, Pte. Ltd., as subsidiary of Supabase, Inc., a Delaware corporation ("**Supabase**") (Customer and Supabase each, a "**Party**" and collectively, the "**Parties**"), effective as of the date of last signature of an Order referencing these terms ("**Effective Date**"), and sets forth the terms and conditions under which Customer may subscribe to certain products and services of Supabase as set forth in one or more Orders (defined below). + +**Accepted and agreed to as of the Effective Date by the authorized representative of each Party by Customer's entering into an Order referencing this Agreement.** + +## 1. Definitions. + +**1.1** "**Aggregated Data**" means data and information related to or derived from Customer Data or Customer's use of the Services that is used by Supabase in an aggregate and anonymized manner, including to compile statistical and performance information related to the Services. + +**1.2** "**Authorized User**" means Customer's employees, consultants, contractors, and agents (a) who are authorized by Customer to access and use the Services under the rights granted to Customer pursuant to this Agreement; and (b) for whom access to the Services has been purchased by Customer hereunder. + +**1.3** "**Customer Data**" means information, data, and other content, in any form or medium, that is submitted, posted, or otherwise transmitted by or on behalf of Customer or an Authorized User through the Services; provided that, for purposes of clarity, Customer Data does not include Aggregated Data. + +**1.4** "**Data Processing Addendum**" means the Data Processing Addendum available at https://supabase.com/legal/customer-resources/data-processing-addendum, or, if the Parties have a separately executed agreement in effect that covers the same subject matter, the separately executed agreement. + +**1.5** "**Documentation**" means Supabase's end user documentation relating to the Services available at supabase.io. + +**1.6** "**Harmful Code**" means any software, hardware, or other technology, device, or means, including any virus, worm, malware, or other malicious computer code, the purpose or effect of which is to permit unauthorized access to, or to destroy, disrupt, disable, distort, or otherwise harm or impede in any manner any (a) computer, software, firmware, hardware, system, or network or (b) any application or function of any of the foregoing or the security, integrity, confidentiality, or use of any data processed thereby. + +**1.7** "**Order**" means: (a) the purchase order, order form, or other ordering document entered into by the Parties that incorporates this Agreement by reference; or (b) if Customer registered for the Services through Supabase's online ordering process, the results of such online ordering process. + +**1.8** "**Personal Information**" means any information that, individually or in combination, does or can identify a specific individual or by or from which a specific individual may be identified, contacted, or located, including without limitation all data considered "personal data", "personally identifiable information", or something similar under applicable laws, rules, or regulations relating to data privacy. + +**1.9** "**Supabase IP**" means the Services, the Documentation, all related and underlying technology, documentation and other information, and any and all intellectual property provided to Customer or any Authorized User in connection with the foregoing, and any derivatives thereof. For the avoidance of doubt, Supabase IP includes Aggregated Data and any information, data, intellectual property or other content derived from Supabase's provision of the Services but does not include Customer Data. + +**1.10** "**Services**" means Supabase's proprietary hosted software platform, as made available by Supabase to Authorized Users from time to time. + +**1.11** "**Subscription Period**" means the period set forth in Section 13.1, below. + +**1.12** "**Third-Party Products**" means any third-party products, software components, or services provided with, integrated with, or incorporated into the Services. + +**1.13** "**Usage Limitations**" means the usage limitations set forth in this Agreement and the Order, including without limitation any limitations on the number of Authorized Users (if any), and the applicable product, pricing, and support tiers agreed-upon by the Parties. + +## 2. Access and Use. + +### 2.1 Provision of Access. + +Subject to and conditioned on Customer's compliance with the terms and conditions of this Agreement, including without limitation the Usage Limitations, Supabase will make available to Customer during the Subscription Period, on a non-exclusive, non-transferable (except in compliance with Section 14.8), and non-sublicensable basis, access to and use of the Services, solely for use by Authorized Users. Such use is limited to Customer's internal business purposes and the features and functionalities specified in the Order. Supabase shall provide to Customer the necessary access credentials to allow Customer to access the Services. Each Order will be incorporated into, and is fully governed by, this Agreement upon execution of the Order by both Parties. + +### 2.2 Documentation License. + +Subject to and conditioned on Customer's compliance with the terms and conditions of this Agreement, Supabase hereby grants to Customer a non-exclusive, non-transferable (except in compliance with Section 14.8), and non-sublicensable license to use the Documentation during the Subscription Period solely for Customer's internal business purposes in connection with its use of the Services. + +### 2.3 Use Restrictions. + +Customer shall not use the Services for any purposes beyond the scope of the access granted in this Agreement. Customer shall not at any time, directly or indirectly, and shall not permit any Authorized Users to: (a) copy, modify, or create derivative works of any Supabase IP, whether in whole or in part; (b) rent, lease, lend, sell, license, sublicense, assign, distribute, publish, transfer, or otherwise make available the Services or Documentation to any third party; (c) reverse engineer, disassemble, decompile, decode, adapt, or otherwise attempt to derive or gain access to any software component of the Services, in whole or in part; (d) remove any proprietary notices from any Supabase IP; (e) use any Supabase IP in any manner or for any purpose that infringes, misappropriates, or otherwise violates any intellectual property right or other right of any person, or that violates any applicable law; (f) access or use any Supabase IP for purposes of competitive analysis of Supabase or the Services, the development, provision, or use of a competing software service or product, or any other purpose that is to Supabase's detriment or commercial disadvantage; (g) bypass or breach any security device or protection used by the Services or access or use the Services other than by an Authorized User through the use of valid access credentials; or (h) input, upload, transmit, or otherwise provide to or through the Services any information or materials that are unlawful or injurious, or that contain, transmit, or activate any Harmful Code. + +### 2.4 Use by Affiliates. + +Each of Customer's Affiliates (defined below) identified on an Order will be entitled to access and use the applicable Supabase IP in accordance with this Agreement and the applicable Order; provided that Customer shall remain responsible to Supabase for the actions and omissions of each such Affiliate (and each of such Affiliate's Authorized Users). The terms of this Agreement will govern, and will be incorporated by reference in, each such Order as if this Agreement were separately executed by the applicable Customer Affiliate, and the term "Customer" as used in this Agreement will be deemed as applying to such Affiliate of Customer for the purposes of such Order. "**Affiliate**" means an entity that, directly or indirectly, controls, is controlled by, or is under common control with a Party. As used herein, "**control**" means the power to direct the management or affairs of an entity or the beneficial ownership of fifty percent (50%) or more of the voting equity securities or other equivalent voting interests of an entity. + +### 2.5 Reservation of Rights. + +Supabase reserves all rights not expressly granted to Customer in this Agreement. Except for the limited rights and licenses expressly granted under this Agreement, nothing in this Agreement grants, by implication, waiver, estoppel, or otherwise, to Customer or any third party any intellectual property rights or any other right, title, or interest in or to the Supabase IP. + +### 2.6 Suspension. + +Notwithstanding anything to the contrary in this Agreement, Supabase may temporarily suspend Customer's and any Authorized User's access to any portion or all of the Services if: (a) Supabase reasonably determines that (i) there is a threat or attack on any of the Supabase IP, (ii) Customer's or any Authorized User's use of the Supabase IP disrupts or poses a security risk to the Supabase IP or to any other customer or vendor of Supabase, (iii) Customer, or any Authorized User, is using the Supabase IP for fraudulent or illegal activities, (iv) subject to applicable law, Customer has ceased to continue its business in the ordinary course, made an assignment for the benefit of creditors or similar disposition of its assets, or become the subject of any bankruptcy, reorganization, liquidation, dissolution, or similar proceeding, or (v) Supabase's provision of the Services to Customer or any Authorized User is prohibited by applicable law; (b) any vendor of Supabase has suspended or terminated Supabase's access to or use of any Third-Party Products required to enable Customer to access the Services; or (c) in accordance with Section 5.1 (any such suspension described in subclause (a), (b), or (c), a "**Service Suspension**"). Supabase shall use commercially reasonable efforts to provide prior written notice of any Service Suspension to Customer and to provide updates regarding resumption of access to the Services following any Service Suspension. Supabase shall use commercially reasonable efforts to resume providing access to the Services as soon as reasonably possible after the event giving rise to the Service Suspension is cured. Supabase will have no liability for any damage, liabilities, losses (including any loss of data or profits), or any other consequences that Customer or any Authorized User may incur as a result of a Service Suspension. + +### 2.7 Aggregated Data. + +Notwithstanding anything to the contrary in this Agreement, Supabase may monitor Customer's use of the Services and collect and compile Aggregated Data. As between Supabase and Customer, all right, title, and interest in Aggregated Data, and all intellectual property rights therein, belong to and are retained solely by Supabase. Customer acknowledges that Supabase may compile Aggregated Data based on Customer Data input into the Services. Customer agrees that Supabase may (a) make Aggregated Data available to third parties including its other customers in compliance with applicable law, and (b) use Aggregated Data to the extent and in the manner permitted under applicable law. + +### 2.8 Service Level Agreement. + +Supabase will make commercially reasonable efforts to provide the Services in accordance with the uptime commitment, service credit, and other terms available at: https://supabase.com/docs/company/sla ("**Service Level Agreement**"). + +## 3. Customer Responsibilities. + +### 3.1 General. + +Customer is responsible and liable for all uses of the Services and Documentation resulting from access provided by Customer, directly or indirectly, whether such access or use is permitted by or in violation of this Agreement. Without limiting the generality of the foregoing, Customer is responsible for all acts and omissions of Authorized Users, and any act or omission by an Authorized User that would constitute a breach of this Agreement if taken by Customer will be deemed a breach of this Agreement by Customer. Customer shall use reasonable efforts to make all Authorized Users aware of this Agreement's provisions as applicable to such Authorized User's use of the Services and shall cause Authorized Users to comply with such provisions. + +### 3.2 Third-Party Terms. + +Supabase may from time to time make Third-Party Products available to Customer or Supabase may allow for certain Third-Party Products to be integrated with the Services to allow for the transmission of Customer Data from such Third-Party Products into the Services. For purposes of this Agreement, such Third-Party Products are subject to their own terms and conditions. To the extent there is a conflict between the terms and conditions applicable to any such Third-Party Product and this Agreement, the Third-Party Product terms and conditions shall control. If Customer does not agree to abide by the applicable terms for any such Third-Party Products, then Customer should not install or use such Third-Party Products. By authorizing Supabase to transmit Customer Data from Third-Party Products into the Services, Customer represents and warrants to Supabase that it has all right, power, and authority to provide such authorization. ALL THIRD-PARTY PRODUCTS ARE MADE AVAILABLE ON AN "AS IS" BASIS WITHOUT WARRANTY OF ANY KIND. IF CUSTOMER USES ANY THIRD-PARTY PRODUCTS, SUPABASE WILL NOT BE RESPONSIBLE FOR ANY ACT OR OMISSION OF ANY PROVIDER OF SUCH THIRD-PARTY PRODUCTS. CUSTOMER ACKNOWLEDGES AND AGREES THAT SUPABASE WILL HAVE NO RESPONSIBILITY OR LIABILITY FOR THE ACTS OR OMISSIONS OF ANY AUTHORIZED USERS IN CONNECTION WITH ANY THIRD-PARTY PRODUCTS. + +### 3.3 Customer Control and Responsibility. + +Customer has and will retain sole responsibility for: (a) all Customer Data, including its content, accuracy, legality and use; (b) all information, instructions, and materials provided by or on behalf of Customer or any Authorized User in connection with the Services; (c) Customer's information technology infrastructure, including computers, software, databases, electronic systems (including database management systems), and networks, whether operated directly by Customer or through the use of third-party services ("**Customer Systems**"); (d) the security and use of Customer's and its Authorized Users' access credentials; and (e) all access to and use of the Services directly or indirectly by or through the Customer Systems or its or its Authorized Users' access credentials, with or without Customer's knowledge or consent, including all results obtained from, and all conclusions, decisions, and actions based on, such access or use. + +### 3.4 Responsible Use of AI-Powered Tools. + +From time to time, Supabase may make available chatbots or other artificial intelligence-enabled tools to assist with customer support inquiries or other interactions (each, an "**AI Tool**"). Customer is responsible for the responsible and informed use of any AI Tool, including by: (a) ensuring that any data, content, prompts, or other materials Customer or its Authorized Users submit to an AI Tool (collectively, "**AI Input**") (i) are submitted with all necessary rights, consents, and authorizations, including with respect to any third-party or personal information contained therein, and (ii) do not violate applicable law, infringe or misappropriate any third-party right, or breach any obligation of confidentiality owed to a third party; (b) applying human judgment to review, evaluate, and verify any output generated by an AI Tool ("**AI Output**") before relying on it, using it or otherwise acting on it; (c) not treating any AI Output as legal, financial, security, or other professional advice; and (d) escalating to a human Supabase representative through Supabase's standard support channels whenever Customer requires authoritative confirmation, has a time-sensitive issue, or is otherwise not satisfied with the AI Output. + +## 4. Support. + +During the Subscription Period, Supabase will use commercially reasonable efforts to provide Customer with basic customer support via Supabase's standard support channels during Supabase's normal business hours. + +## 5. Fees and Taxes. + +### 5.1 Fees and Payment. + +Where paid-for services are agreed between Supabase and Customer per the applicable Order, Customer shall pay Supabase the fees ("**Fees**") identified in the Order without offset or deduction at the cadence identified in the Order (e.g., monthly or annually). Customer will pay the Fees within thirty (30) days of the issuance of an invoice. Except as otherwise set forth in the applicable Order, Fees are payable by credit card or other payment method permitted by Supabase from time to time, payment will be in USD, and Customer authorizes Supabase to charge its credit card or bank account for all Fees. Customer further authorizes Supabase to use a third party to process payments, and consents to the disclosure of Customer's payment information to such third party. Fees paid by Customer are non-refundable (except as set forth in Sections 10.3 or 11.1(b)). If Customer fails to make any payment when due, and Customer has not notified Supabase in writing within ten (10) days of the payment becoming due and payable that the payment is subject to a good faith dispute, without limiting Supabase's other rights and remedies: (a) Supabase may charge interest on the undisputed past due amount at the rate of 1.5% per month, calculated daily and compounded monthly or, if lower, the highest rate permitted under applicable law; (b) Customer shall reimburse Supabase for all reasonable costs incurred by Supabase in collecting any late payments or interest, including attorneys' fees, court costs, and collection agency fees; and (c) if such failure continues for ten (10) days or more, Supabase may suspend Customer's and its Authorized Users' access to any portion or all of the Services until such amounts are paid in full. + +### 5.2 Taxes. + +All Fees and other amounts payable by Customer under this Agreement are exclusive of taxes and similar assessments. Customer is responsible for all sales, use, and excise taxes, and any other similar taxes, duties, and charges of any kind imposed by any federal, state, or local governmental or regulatory authority on any amounts payable by Customer hereunder, other than any taxes imposed on Supabase's income. To the extent that Supabase is required by law to pay any such taxes, duties, or other charges to any governmental or regulatory authority, Supabase may invoice Customer for such taxes, duties, or other charges and Customer will pay such invoiced amounts in accordance with this Agreement. + +## 6. Confidential Information. + +### 6.1 Definition. + +From time to time during the Subscription Period, either Party may disclose or make available to the other Party information about its business affairs, products, confidential intellectual property, trade secrets, third-party confidential information, and other sensitive or proprietary information, whether orally or in written, electronic, or other form or media that: (a) is marked, designated or otherwise identified as "confidential" or something similar at the time of disclosure or within a reasonable period of time thereafter; or (b) would be considered confidential by a reasonable person given the nature of the information or the circumstances of its disclosure (collectively, "**Confidential Information**"). Except for Personal Information, Confidential Information does not include information that, at the time of disclosure is: (i) in the public domain; (ii) known to the receiving Party at the time of disclosure; (iii) rightfully obtained by the receiving Party on a non-confidential basis from a third party; or (iv) independently developed by the receiving Party without use of, reference to, or reliance upon the disclosing Party's Confidential Information, as proven by the receiving Party's then-contemporaneous written records. + +### 6.2 Duty. + +The receiving Party shall not disclose the disclosing Party's Confidential Information to any person or entity, except to the receiving Party's employees, contractors, and agents who have a need to know the Confidential Information for the receiving Party to exercise its rights or perform its obligations hereunder ("**Representatives**"). The receiving Party will be responsible for all the acts and omissions of its Representatives as they relate to Confidential Information hereunder. Notwithstanding the foregoing, each Party may disclose Confidential Information to the limited extent required (a) in order to comply with the order of a court or other governmental body, or as otherwise necessary to comply with applicable law, provided that the Party making the disclosure pursuant to the order shall first have given written notice to the other Party and made a reasonable effort to obtain a protective order, or (b) to establish a Party's rights under this Agreement, including to make required court filings. Further, notwithstanding the foregoing, each Party may disclose the terms and existence of this Agreement to its actual or potential investors, debtholders, acquirers, or merger partners under customary confidentiality terms. + +### 6.3 Return of Materials; Effects of Termination / Expiration. + +On the expiration or termination of this Agreement, the receiving Party shall promptly return to the disclosing Party all copies, whether in written, electronic, or other form or media, of the disclosing Party's Confidential Information, or destroy all such copies and certify in writing to the disclosing Party that such Confidential Information has been destroyed. Each Party's obligations of non-use and non-disclosure with regard to Confidential Information are effective as of the Effective Date and will expire three (3) years from the date of termination or expiration of this Agreement; provided, however, with respect to any Confidential Information that constitutes a trade secret (as determined under applicable law), such obligations of non-disclosure will survive the termination or expiration of this Agreement for as long as such Confidential Information remains subject to trade secret protection under applicable law. + +## 7. Personal Information. + +### 7.1 General. + +Customer will ensure that its Customer Data, and its use of such Customer Data, complies with this Agreement and any applicable local, state, federal and international laws, regulations and conventions, including, without limitation, those related to data privacy, international communications, and the exportation of technical or personal data (including Personal Information). Customer is responsible for properly configuring and using the Services and taking its own steps to maintain appropriate security, protection, and backup of Customer Data. Customer may not store or process protected health information (as defined in HIPAA) using the Services unless Customer signs a Business Associate Agreement with Supabase. Customer may not store any payment cardholder information using the Services without Supabase's prior written approval. + +### 7.2 Data Processing. + +Customer will ensure that its Customer Data, and its use of such Customer Data, complies with this Agreement and any applicable law. Customer is responsible for properly configuring and using the Services and taking its own steps to maintain appropriate security, protection, and backup of Customer Data. Customer may not store or process protected health information (as defined in HIPAA) using the Services unless Customer signs a Business Associate Agreement with Supabase. Customer may not store any payment cardholder information using the Services without Supabase's prior written approval. The Parties agree to comply with the Data Processing Addendum, which is incorporated into this Agreement. + +## 8. Security. + +Supabase shall use reasonable physical, technical, and administrative procedures designed to protect, safeguard and help prevent loss, misuse, and unauthorized access, disclosure, alteration or destruction of Customer Data, and Supabase will choose these safeguards based on the sensitivity of the information that is collected, processed, and stored and the current state of applicable technology. + +## 9. Intellectual Property Ownership; Feedback. + +### 9.1 Supabase IP. + +Customer acknowledges that, as between Customer and Supabase, Supabase owns all right, title, and interest, including all intellectual property rights, in and to the Supabase IP and, with respect to Third-Party Products, the applicable third-party providers own all right, title, and interest, including all intellectual property rights, in and to the Third-Party Products. + +### 9.2 Customer Data. + +Supabase acknowledges that, as between Supabase and Customer, Customer owns all right, title, and interest, including all intellectual property rights, in and to the Customer Data. Customer hereby grants to Supabase a non-exclusive, royalty-free, worldwide license to reproduce, distribute, and otherwise use and display the Customer Data and perform all acts with respect to the Customer Data as may be necessary for Supabase to provide the Services to Customer. Customer may export the Customer Data at any time through the features and functionalities made available via the Services. To the extent Supabase utilizes artificial intelligence to provide features to Customer via the Services, and in Supabase's provision of the AI Tools, Supabase will not use, nor allow any third-party to use, Customer Data, Customer's AI Input, or any AI Output to train, fine-tune, or otherwise improve any artificial intelligence or machine learning model, without Customer's prior written consent. For the avoidance of doubt, Customer retains all right, title and interest in Customer Data contained in AI Inputs and AI Outputs. + +### 9.3 Feedback. + +If Customer or any of its employees or contractors sends or transmits any communications or materials to Supabase by mail, email, telephone, or otherwise, suggesting or recommending changes to the Supabase IP, including without limitation, new features or functionality relating thereto, or any comments, questions, suggestions, or the like ("**Feedback**"), Customer hereby assigns to Supabase all right, title and interest in and to the Feedback, including all intellectual property rights therein, and acknowledges that Supabase shall own such Feedback. + +## 10. Warranties. + +### 10.1 Mutual. + +Each Party represents and warrants to the other Party that: + +(a) it is duly organized, validly existing, and in good standing as a corporation or other entity under the Laws of the jurisdiction of its incorporation or other organization; + +(b) it has the full right, power, and authority to enter into and perform its obligations and grant the rights, licenses, consents, and authorizations it grants or is required to grant under this Agreement; + +(c) the execution of this Agreement by its representative whose signature is set forth at the end of this Agreement has been duly authorized by all necessary corporate or organizational action of such party; and + +(d) when executed and delivered by both parties, this Agreement will constitute the legal, valid, and binding obligation of such party, enforceable against such party in accordance with its terms. + +### 10.2 Customer. + +Customer represents, warrants, and covenants to Supabase that Customer owns or otherwise has and will have the necessary rights and consents in and relating to the Customer Data so that, as received by Supabase and processed in accordance with this Agreement, they do not and will not infringe, misappropriate, or otherwise violate any intellectual property, privacy or other rights of any third party or violate any applicable Law. + +### 10.3 Limited Warranty. + +Supabase warrants that it will provide the Services in substantial conformity with the applicable Documentation and the descriptions in the Order. Supabase's sole liability (and Customer's sole and exclusive remedy) for any breach of this warranty shall be, in Supabase's sole discretion and at no charge to Customer, to use commercially reasonable efforts to provide Customer with an error correction or work-around that corrects the reported non-conformity, or if Supabase determines such remedies to be impracticable, to allow Customer to terminate the Subscription Period and receive as its sole remedy and Supabase's entire liability, a refund of any Fees Customer has pre-paid for use of the Services or related services it has not received as of the date of the warranty claim. The limited warranty set forth in this Section 10.3 shall not apply: (a) unless Customer makes a claim within thirty (30) days of the date on which the condition giving rise to the claim first appeared, (b) if the error was caused by misuse, unauthorized modifications or third-party hardware, software or services, or (c) to Services provided on a no-charge or evaluation basis. + +### 10.4 Warranty Disclaimer. + +EXCEPT FOR THE LIMITED WARRANTY SET FORTH IN SECTION 10.3, THE SUPABASE IP IS PROVIDED "AS IS" AND ON AN "AS AVAILABLE" BASIS. SUPABASE HEREBY DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE. SUPABASE SPECIFICALLY DISCLAIMS ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT, AND ALL WARRANTIES ARISING FROM COURSE OF DEALING, USAGE, OR TRADE PRACTICE. SUPABASE MAKES NO WARRANTY OF ANY KIND THAT THE SUPABASE IP, OR ANY PRODUCTS OR RESULTS OF THE USE THEREOF, WILL MEET CUSTOMER'S OR ANY OTHER PERSON'S REQUIREMENTS, OPERATE WITHOUT INTERRUPTION, ACHIEVE ANY INTENDED RESULT, BE COMPATIBLE OR WORK WITH ANY SOFTWARE, SYSTEM OR OTHER SERVICES, OR BE SECURE, ACCURATE, COMPLETE, FREE OF HARMFUL CODE, OR ERROR FREE. In addition, Customer acknowledges that AI Tools are powered by one or more third-party large language models that Supabase does not own or control, and that AI Outputs may be inaccurate, incomplete, or unsuitable for Customer's particular use case. Customer acknowledges that other customers providing similar AI Input may receive similar or the same AI Output. AI TOOLS AND THEIR OUTPUTS ARE PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS, AND SUPABASE MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, AS TO THE ACCURACY, RELIABILITY, COMPLETENESS, ERROR-FREE NATURE, NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE OF ANY OUTPUT GENERATED FROM THE AI TOOLS. + +## 11. Indemnification. + +### 11.1 Supabase Indemnification. + +(a) Supabase shall indemnify, defend, and hold harmless Customer from and against any and all losses, damages, liabilities, costs (including reasonable attorneys' fees) ("**Losses**") incurred by Customer resulting from any claim, suit, action, or proceeding asserted by a third-party ("**Third-Party Claim**") against Customer that the Services, or any use of the Services in accordance with this Agreement, infringes or misappropriates such third party's US copyrights or trade secrets; provided that Customer promptly notifies Supabase in writing of the claim (but in any event notice in sufficient time for Supabase to respond without prejudice), cooperates with Supabase, and allows Supabase sole authority to control the defense and settlement of such claim. + +(b) If such a claim is made or appears possible, Customer agrees to permit Supabase, at Supabase's sole discretion: to (i) modify or replace the Services, or component or part thereof, to make it non-infringing; or (ii) obtain the right for Customer to continue use. If Supabase determines that neither alternative is reasonably commercially available, Supabase may terminate this Agreement, in its entirety or with respect to the affected component or part, effective immediately on written notice to Customer. + +(c) This Section 11.1 will not apply to the extent that the alleged infringement arises from: (i) use of the Services in combination with data, software, hardware, equipment, or technology not provided by Supabase or authorized by Supabase in writing; (ii) modifications to the Services not made by Supabase; (iii) Customer Data; (iv) Third-Party Products; (v) use of the Services in breach of this Agreement; (vi) AI Output; or (vii) AI Input. + +### 11.2 Customer Indemnification. + +Customer shall indemnify, hold harmless, and, at Supabase's option, defend Supabase from and against any Losses resulting from (a) any Third-Party Claim that the Customer Data, AI Input or any use of the Customer Data in accordance with this Agreement, infringes or misappropriates such third party's US intellectual property or other rights, and (b) arising out of or in connection with any claim arising from or relating to Customer's or any Authorized User's: (i) negligence or willful misconduct; (ii) use of the Services in a manner not authorized by this Agreement; (iii) Customer's violation of any laws, regulations, or rights relating to Customer Data or AI Input (including, without limitation, privacy laws, regulations, or rights), or (iv) use of the Services in combination with data, software, hardware, equipment or technology not provided by Supabase or authorized by Supabase in writing; in each case provided that Customer may not settle any Third-Party Claim against Supabase unless Supabase consents to such settlement, and further provided that Supabase will have the right, at its option, to defend itself against any such Third-Party Claim or to participate in the defense thereof by counsel of its own choice. This indemnification obligation is subject to Customer receiving prompt written notice of such claim (but in any event notice in sufficient time for Customer to respond without prejudice). + +### 11.3 Sole Remedy. + +Section 11.1 sets forth Customer's sole remedies and Supabase's sole liability and obligation for any actual, threatened, or alleged claims that the Services infringe, misappropriate, or otherwise violate any intellectual property rights of any third party. + +## 12. Limitations of Liability. + +Except for (a) a Party's breach of its confidentiality obligations, (b) a Party's indemnity obligations, or (c) a Party's gross negligence, fraud, or willful misconduct ("**Excluded Liabilities**"): (i) in no event will either Party be liable under or in connection with this Agreement under any legal or equitable theory, including breach of contract, tort (including negligence), strict liability, and otherwise, for any: (1) consequential, incidental, indirect, exemplary, special, enhanced, or punitive damages; (2) increased costs, diminution in value or lost business, production, contracts, anticipated savings, revenues, or profits; (3) loss of goodwill or reputation; (4) use, inability to use, loss, interruption, delay or recovery of any data, or breach of data or system security; or (5) cost of replacement goods or services, in each case regardless of whether such Party was advised of the possibility of such losses or damages or such losses or damages were otherwise foreseeable; and (ii) in no event will either Party's aggregate liability arising out of or related to this Agreement under any legal or equitable theory, including breach of contract, tort (including negligence), strict liability, and otherwise exceed the total amounts paid and/or payable to Supabase under this Agreement in the twelve (12) months immediately preceding the events giving rise to the applicable claim; provided that, notwithstanding the foregoing, Supabase's aggregate, cumulative liability arising out of or relating to any Excluded Liabilities will not exceed three times (3x) the total amounts paid and/or payable to Supabase by Customer under this Agreement in the twelve (12) months immediately preceding the events giving rise to the applicable claim. + +## 13. Subscription Period and Termination. + +### 13.1 Subscription Period. + +The initial term of this Agreement begins on the Effective Date and, unless terminated earlier pursuant to Section 13.2, will continue in effect for the period identified in the Order (the "**Initial Subscription Period**"). This Agreement will automatically renew for additional successive terms equal to the length of the Initial Subscription Period unless earlier terminated pursuant to this Agreement's express provisions or either Party gives the other Party written notice of non-renewal at least thirty (30) days prior to the expiration of the then-current term (each a "**Renewal Subscription Period**" and together with the Initial Subscription Period, the "**Subscription Period**"). + +### 13.2 Termination. + +In addition to any other express termination right set forth in this Agreement: + +(a) Supabase may terminate this Agreement, effective on written notice to Customer, if Customer: (i) fails to pay any amount when due hereunder, and such failure continues more than ten (10) calendar days after Supabase's delivery of written notice thereof; or (ii) breaches any of its obligations under Section 2.3 or Section 6; + +(b) either Party may terminate this Agreement, effective on written notice to the other Party, if the other Party materially breaches this Agreement, and such breach: (i) is incapable of cure; or (ii) being capable of cure, remains uncured thirty (30) calendar days after the non-breaching Party provides the breaching Party with written notice of such breach; or + +(c) either Party may terminate this Agreement, effective immediately upon written notice to the other Party, if the other Party: (i) becomes insolvent or is generally unable to pay, or fails to pay, its debts as they become due; (ii) files or has filed against it, a petition for voluntary or involuntary bankruptcy or otherwise becomes subject, voluntarily or involuntarily, to any proceeding under any domestic or foreign bankruptcy or insolvency law; (iii) makes or seeks to make a general assignment for the benefit of its creditors; or (iv) applies for or has appointed a receiver, trustee, custodian, or similar agent appointed by order of any court of competent jurisdiction to take charge of or sell any material portion of its property or business. + +### 13.3 Effect of Expiration or Termination. + +Upon expiration or earlier termination of this Agreement, Customer shall immediately discontinue use of the Supabase IP and, without limiting Customer's obligations under Section 6, Customer shall delete, destroy, or return all copies of the Supabase IP and certify in writing to the Supabase that the Supabase IP has been deleted or destroyed. No expiration or termination will affect Customer's obligation to pay all Fees that may have become due before such expiration or termination or entitle Customer to any refund (except as provided under Section 10.3). + +### 13.4 Survival. + +This Section 13.4 and Sections 1, 5, 6, 9, 10, 11.2, 12, and 14 survive any termination or expiration of this Agreement. No other provisions of this Agreement survive the expiration or earlier termination of this Agreement. + +## 14. Miscellaneous. + +### 14.1 Entire Agreement. + +This Agreement, together with any other documents incorporated herein by reference, constitutes the sole and entire agreement of the Parties with respect to the subject matter of this Agreement and supersedes all prior and contemporaneous understandings, agreements, and representations and warranties, both written and oral, with respect to such subject matter. In the event of any inconsistency between the statements made in the body of this Agreement, the related Exhibits, and any other documents incorporated herein by reference, the following order of precedence governs: (a) first, this Agreement; (b) second, any Order; and (c) any other documents incorporated herein by reference. This Agreement may be executed electronically and in counterparts (such as via DocuSign), which counterparts taken together shall form one legal instrument. Any pre-printed terms in a Customer purchase order or similar document are null and void. + +### 14.2 Notices. + +All notices, requests, consents, claims, demands, waivers, and other communications hereunder (each, a "**Notice**") must be in writing and addressed to the Parties at the addresses set forth on the first page of this Agreement or as identified on the Order (or to such other address that may be designated by the Party giving Notice from time to time in accordance with this Section 14.2). All Notices must be delivered by personal delivery, nationally recognized signed for on delivery courier (with all fees pre-paid), or email (with confirmation of transmission). All email Notices to Supabase must be sent to legal@supabase.io. Except as otherwise provided in this Agreement, a Notice is effective only: (a) upon receipt by the receiving Party; and (b) if the Party giving the Notice has complied with the requirements of this Section 14.2. + +### 14.3 Force Majeure. + +In no event shall either Party be liable to the other Party, or be deemed to have breached this Agreement, for any failure or delay in performing its obligations under this Agreement (except for any obligations to make payments), if and to the extent such failure or delay is caused by any circumstances beyond such Party's reasonable control, including but not limited to acts of God, flood, fire, earthquake, explosion, war, terrorism, invasion, riot or other civil unrest, strikes, labor stoppages or slowdowns or other industrial disturbances, or passage of law or any action taken by a governmental or public authority, including imposing an embargo. + +### 14.4 Amendment and Modification. + +Supabase may modify this Agreement at any time by posting a revised version at https://supabase.com/enterprise-terms, which modification will become effective as of the first day of the calendar month following the month in which it was first posted; provided, however, that if an Order specifies a fixed Subscription Period of twelve (12) months or longer, the modification will instead be effective immediately upon the start of the next Renewal Subscription Period. In either case, if Customer objects to the updated Agreement, as its sole and exclusive remedy, Customer may elect not to renew in accordance with Section 13.1. For the avoidance of doubt, any Order is subject to the version of this Agreement in effect at the time of the Order. Purchase orders (and similar documents) issued by Customer are for administrative purposes only (e.g. setting forth products and services ordered and associated fees) and any additional or different terms or conditions contained in any such order shall not apply (even if the order is accepted or performed on by Supabase). + +### 14.5 Waiver. + +No failure or delay by either Party in exercising any right or remedy available to it in connection with this Agreement will constitute a waiver of such right or remedy. No waiver under this Agreement will be effective unless made in writing and signed by an authorized representative of the Party granting the waiver. + +### 14.6 Severability. + +If any provision of this Agreement is adjudged by any court of competent jurisdiction to be invalid, illegal, or unenforceable in any jurisdiction, such invalidity, illegality, or unenforceability will not affect any other term or provision of this Agreement or invalidate or render unenforceable such term or provision in any other jurisdiction. Upon such determination that any term or other provision is invalid, illegal, or unenforceable, the Parties shall negotiate in good faith to modify this Agreement so as to effect their original intent as closely as possible in a mutually acceptable manner in order that the transactions contemplated hereby be consummated as originally contemplated to the greatest extent possible. + +### 14.7 Governing Law; Submission to Jurisdiction; Dispute Resolution. + +This Agreement is governed by and construed in accordance with the internal laws of the State of California without giving effect to any choice or conflict of law provision or rule that would require or permit the application of the laws of any jurisdiction other than those of the State of California. All disputes arising out of or in connection with this Agreement between the Parties shall be finally settled under the Rules of Arbitration of the International Chamber of Commerce by one or more arbitrators appointed in accordance with such Rules of Arbitration, subject to the terms of this Section 14.7. The place of arbitration shall be: (a) Singapore to the extent that Customer is located in Asia; (b) London, United Kingdom to the extent that Customer is located in Europe; or (c) San Francisco County, California to the extent that Customer is located in the United States or any other jurisdiction. The language of the arbitration shall be English. Nothing in this Section 14.7 will be deemed as preventing Supabase from seeking injunctive or other equitable relief from the courts as necessary to prevent the actual or threatened infringement, misappropriation, or violation of Supabase's data security, confidential information, or intellectual property rights. Customer agrees to submit to the exclusive personal jurisdiction of the federal and state courts located in California for any actions for which Supabase retains the right to seek injunctive or other equitable relief in a court of competent jurisdiction to prevent the actual or threatened infringement, misappropriation, or violation of data security, Confidential Information, or intellectual property rights, including any provisional relief required to prevent irreparable harm. + +### 14.8 Assignment. + +Customer may not assign any of its rights or delegate any of its obligations hereunder, in each case whether voluntarily, involuntarily, by operation of law or otherwise, without the prior written consent of Supabase. Any purported assignment or delegation in violation of this Section 14.8 will be null and void. No assignment or delegation will relieve the assigning or delegating Party of any of its obligations hereunder. This Agreement is binding upon and inures to the benefit of the Parties and their respective permitted successors and assigns. + +### 14.9 Export Regulation. + +The Services utilize software and technology that may be subject to US export control laws, including the US Export Administration Act and its associated regulations. Each Party shall comply with all applicable export and re-export control and trade and economic sanctions laws. Customer shall not, directly or indirectly, export, re-export, or release the Services or the underlying software or technology to, or make the Services or the underlying software or technology accessible from, any jurisdiction or country to which export, re-export, or release is prohibited by law, rule, or regulation. Customer shall comply with all applicable federal laws, regulations, and rules, and complete all required undertakings (including obtaining any necessary export license or other governmental approval), prior to exporting, re-exporting, releasing, or otherwise making the Services or the underlying software or technology available outside the US. Neither Party, nor any of its subsidiaries or any person acting on its behalf or owning 50% or more of its equity securities or other equivalent voting interests, is (a) a person on the List of Specially Designated Nationals and Blocked Persons or any other list of sanctioned persons administered by OFAC or any other governmental entity, or (b) a national or resident of, or a segment of the government of, any country or territory for which the United States has embargoed goods or imposed trade sanctions. + +### 14.10 US Government Rights. + +Each of the Documentation and the software components that constitute the Services is a "commercial item" as that term is defined at 48 C.F.R. § 2.101, consisting of "commercial computer software" and "commercial computer software documentation" as such terms are used in 48 C.F.R. § 12.212. Accordingly, if Customer is an agency of the US Government or any contractor therefor, Customer only receives those rights with respect to the Services and Documentation as are granted to all other end users, in accordance with (a) 48 C.F.R. § 227.7201 through 48 C.F.R. § 227.7204, with respect to the Department of Defense and their contractors, or (b) 48 C.F.R. § 12.212, with respect to all other US Government users and their contractors. + +### 14.11 Equitable Relief. + +Each Party acknowledges and agrees that a breach or threatened breach by such Party of any of its obligations under Section 6 or, in the case of Customer, Section 2.3, would cause the other Party irreparable harm for which monetary damages would not be an adequate remedy and agrees that, in the event of such breach or threatened breach, the other Party will be entitled to equitable relief, including a restraining order, an injunction, specific performance and any other relief that may be available from any court, without any requirement to post a bond or other security, or to prove actual damages or that monetary damages are not an adequate remedy. Such remedies are not exclusive and are in addition to all other remedies that may be available at law, in equity or otherwise. + +### 14.12 Publicity. + +Supabase may identify Customer as a user of the Services and may use Customer's name, logo, and other trademarks in Supabase's customer list, press releases, blog posts, advertisements, and website (and all use thereof and goodwill arising therefrom shall inure to the sole and exclusive benefit of Customer). Otherwise, neither Party may use the name, logo, or other trademarks of the other Party for any purpose without the other Party's prior written approval. Customer may opt out of the foregoing publicity rights at any time by providing written notice to Supabase at legal@supabase.io, and Supabase will cease any such use within a commercially reasonable period. + +### 14.13 Subcontractors. + +Supabase may use the services of subcontractors for performance of services under this Agreement, provided that Supabase remains responsible for such subcontractors' compliance with the terms of this Agreement. + +### 14.14 Independent Contractors. + +The Parties to this Agreement are independent contractors. There is no relationship of partnership, joint venture, employment, franchise or agency created hereby between the Parties. Neither Party will have the power to bind the other or incur obligations on the other Party's behalf without the other Party's prior written consent. + +### 14.15 No Third Party Rights. + +There are no third-party beneficiaries to this Agreement. diff --git a/apps/www/pages/enterprise-terms.tsx b/apps/www/pages/enterprise-terms.tsx index fdbfe1e5137..be4507aeef0 100644 --- a/apps/www/pages/enterprise-terms.tsx +++ b/apps/www/pages/enterprise-terms.tsx @@ -4,6 +4,7 @@ import SectionContainer from '~/components/Layouts/SectionContainer' import LegalDocVersions, { type LegalDocVersion } from '~/components/Legal/LegalDocVersions' import V1 from '~/data/legal/enterprise-terms/v1.mdx' import V2 from '~/data/legal/enterprise-terms/v2.mdx' +import V3 from '~/data/legal/enterprise-terms/v3.mdx' import mdxComponents from '~/lib/mdx/mdxComponents' import { NextSeo } from 'next-seo' @@ -15,6 +16,7 @@ const meta = { } const versions: LegalDocVersion[] = [ + { id: 'v3', label: 'Version 3', effectiveDate: 'August 1, 2026', Component: V3 }, { id: 'v2', label: 'Version 2', effectiveDate: 'May 6, 2026', Component: V2 }, { id: 'v1', label: 'Version 1', effectiveDate: 'April 17, 2026', Component: V1 }, ] From 76a9ba968c9145d85d6c1b4f643df61719a01847 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:05:46 +0200 Subject: [PATCH 121/141] refactor(www): unify legal page shells and versioning (#48483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _Requested by **Francesco Sansalvadore, Nicole Kramer** · [Slack thread](https://supabase.slack.com/archives/C0161K73J1J/p1785399057853249?thread_ts=1785399057.853249&cid=C0161K73J1J)_ ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Refactor of the marketing site's legal pages, plus two small content fixes (removal of duplicated dates, two heading corrections) and two permanent redirects. ## What is the current behavior? The documents linked from the Legal Hub are built three different ways: - `/terms` and `/enterprise-terms` render a plain inline heading with no breadcrumbs. - `/sla`, `/support-policy`, `/aup` and `/privacy` are standalone MDX pages carrying their own layout. - `/legal/dpa` has a one-off centered heading and grid of its own. On top of that, the documents that already have a version selector *also* print a "Last Modified" line inside the document body, so the same fact is stated twice on the page. On `/terms` and `/enterprise-terms` the two statements disagree: the selector says "Version 2 — May 6, 2026" while the body says "Last Modified: 1 May 2026". `/privacy` handles its history differently again — earlier versions live at their own archived URLs (`/privacy-260316` and `/privacy-250528`), strung together by "Previous Version" links at the bottom of each page. ## What is the new behavior? Every legal page now renders through one shell: `PageHeader` with a `PageBreadcrumb`, so the Legal Hub is one click away from any document. - The duplicate "Last Modified" rows are removed from the five versioned documents. The version selector is now the single place a date appears. - `/aup` and `/privacy` gain the version selector. - `/privacy`'s three historical versions are now selectable from the one page, and the two old archived URLs permanently redirect to it. - `/sla` and `/support-policy` pick up the shell and breadcrumbs but intentionally show neither a date nor a selector — neither document has ever carried one, and Legal asked that the SLA stay that way for now. Implementation-wise the canonical pattern is the one the Data Processing Addendum page was already using: `DefaultLayout` > `NextSeo` > `PageHeader` (with a `breadcrumb` slot) > `MDXProvider` > `SectionContainer className="prose"` > `LegalDocVersions`. The standalone MDX pages were moved to `apps/www/data/legal//vN.mdx` as bare content partials, with a new TSX shell taking over the original route. No route changed except the two archived privacy pages, which redirect. Dates were carried across from the "Last Modified" lines being deleted rather than invented: `/aup` becomes Version 1 — June 1, 2026, and privacy v1/v2/v3 become May 28 2025, March 16 2026 and May 13 2026. ## Additional context **This is a stacked PR.** It is sequenced behind three PRs that touch the same files and should land first: the Terms of Service v3 bump, the Enterprise SaaS Subscription Agreement v3 bump, and #48481 (DPA effective date → August 1, 2026). #48481 edits the very "Last Modified" line this PR removes from the DPA content file, so a trivial conflict there is expected. This branch will be rebased onto master before it leaves draft. **Two contracts now contain a clause that no longer describes the page.** `apps/www/data/legal/terms/v1.mdx` and `v2.mdx` — and the same sentence in the MPPA and both integration-partner addenda — still read "The date on which the Agreement was last modified will be updated at the top of this Agreement". There is no longer a date in the document body; it sits in the version selector above it. Left untouched here because it is contract text, but Legal should re-word it. **The date mismatch is resolved in favour of the selector.** On `/terms` and `/enterprise-terms`, deleting the body line leaves May 6, 2026 as the only date on the page. Nicole Kramer confirmed in Slack that May 6 is the correct date. **Two headings change visibly**, to line up with the labels used on the Legal Hub: "Terms of service" → "Terms of Service", and "Service Level Agreements" → "Service Level Agreement". **`/legal/dpa` now looks almost identical to `/legal/customer-resources/data-processing-addendum`** — same heading, same breadcrumb, different content. The legacy page is a PDF download plus a signing flow and was deliberately left live, but the overlap is more obvious than it was. Redirecting it to the versioned page is the natural follow-up; it is not done here. **Build verification was incomplete in this environment.** `pnpm install` could not finish because `npm.jsr.io` is blocked by network policy (403), so `next build` never gave a real signal. What did run and pass: - `tsc --noEmit` on `apps/www`, with output byte-identical to clean master - ESLint on every changed file — 0 errors - Prettier using the repo's actual config - a direct MDX compile of all 14 `data/legal/**/*.mdx` files using the app's own MDX options The one thing left unverified is webpack resolving `ui-patterns/PrivacySettings` from the privacy content's new directory. CI will confirm that. Co-authored-by: Claude --- .../{pages/aup.mdx => data/legal/aup/v1.mdx} | 18 --- .../data-processing-addendum/v1.mdx | 2 - apps/www/data/legal/enterprise-terms/v1.mdx | 2 - apps/www/data/legal/enterprise-terms/v2.mdx | 2 - apps/www/data/legal/enterprise-terms/v3.mdx | 2 - .../legal/privacy/v1.mdx} | 19 ---- .../legal/privacy/v2.mdx} | 19 ---- .../privacy.mdx => data/legal/privacy/v3.mdx} | 20 ---- .../{pages/sla.mdx => data/legal/sla/v1.mdx} | 16 --- .../legal/support-policy/v1.mdx} | 16 --- apps/www/data/legal/terms/v1.mdx | 2 - apps/www/data/legal/terms/v2.mdx | 2 - apps/www/data/legal/terms/v3.mdx | 2 - apps/www/lib/redirects.js | 10 ++ apps/www/pages/aup.tsx | 35 ++++++ apps/www/pages/enterprise-terms.tsx | 15 ++- apps/www/pages/legal/dpa.tsx | 107 ++++++++++-------- apps/www/pages/privacy.tsx | 39 +++++++ apps/www/pages/sla.tsx | 37 ++++++ apps/www/pages/support-policy.tsx | 37 ++++++ apps/www/pages/terms.tsx | 22 +++- 21 files changed, 242 insertions(+), 182 deletions(-) rename apps/www/{pages/aup.mdx => data/legal/aup/v1.mdx} (93%) rename apps/www/{pages/privacy-250528.mdx => data/legal/privacy/v1.mdx} (99%) rename apps/www/{pages/privacy-260316.mdx => data/legal/privacy/v2.mdx} (99%) rename apps/www/{pages/privacy.mdx => data/legal/privacy/v3.mdx} (99%) rename apps/www/{pages/sla.mdx => data/legal/sla/v1.mdx} (97%) rename apps/www/{pages/support-policy.mdx => data/legal/support-policy/v1.mdx} (92%) create mode 100644 apps/www/pages/aup.tsx create mode 100644 apps/www/pages/privacy.tsx create mode 100644 apps/www/pages/sla.tsx create mode 100644 apps/www/pages/support-policy.tsx diff --git a/apps/www/pages/aup.mdx b/apps/www/data/legal/aup/v1.mdx similarity index 93% rename from apps/www/pages/aup.mdx rename to apps/www/data/legal/aup/v1.mdx index 22a8f22d7f2..3413a578dfe 100644 --- a/apps/www/pages/aup.mdx +++ b/apps/www/data/legal/aup/v1.mdx @@ -1,17 +1,3 @@ -import Layout from '../layouts/Layout' -import SectionContainer from '../components/Layouts/SectionContainer' - -export const meta = { - title: 'Acceptable Use Policy | Supabase', - description: 'Supabase Acceptable Use Policy', -} - - - -# Acceptable Use Policy - -_Last Modified: 1 June 2026_ - This Acceptable Use Policy (this "Policy") describes prohibited uses of the web services offered by Supabase, Inc. and its affiliates (the "Services") and the website located at https://supabase.com (the "Supabase Site"). By using the Services or accessing the Supabase Site, you agree to the latest version of this Policy. Violation of this Policy may result in the immediate suspension or termination of your Accounts and / or Services. @@ -61,7 +47,3 @@ By using the Services or accessing the Supabase Site, you agree to the latest ve ## 7. Reporting Violations Report violations to legal@supabase.io or abuse@supabase.io - - - -export default (context) => diff --git a/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx b/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx index a62a4bbb4dd..70d330bc66a 100644 --- a/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx +++ b/apps/www/data/legal/customer-resources/data-processing-addendum/v1.mdx @@ -1,5 +1,3 @@ -_Last Modified: 1 August 2026_ - This Data Processing Addendum (the “**DPA**”) supplements and forms part of the Supabase Terms of Service available at [https://supabase.com/terms](https://supabase.com/terms), or such other agreement entered into between the Customer and Supabase Pte. Ltd (“**Supabase**”) relevant to Customer’s use of the Services (the “**Agreement**”), and in case of any conflict, supersedes the Agreement in relation to the transfer and processing of Covered Data in connection with the performance of the Services. This DPA is effective as of the Effective Date of the Agreement. ## 1. Definitions. diff --git a/apps/www/data/legal/enterprise-terms/v1.mdx b/apps/www/data/legal/enterprise-terms/v1.mdx index fdc28888efc..96a74df6584 100644 --- a/apps/www/data/legal/enterprise-terms/v1.mdx +++ b/apps/www/data/legal/enterprise-terms/v1.mdx @@ -1,5 +1,3 @@ -_Last Modified: 17 April 2026_ - This Enterprise SaaS Subscription Agreement (this "**Agreement**") is entered into by and between the customer identified in the signature to the Order which references these terms ("**Customer**") and the Supabase Affiliate entity identified in such Order ("**Supabase**") (Customer and Supabase each, a "**Party**" and collectively, the "**Parties**"), and is effective as of the Services Start Date identified in such Order ("**Effective Date**"), and sets forth the terms and conditions under which Customer may subscribe to certain products and services of Supabase as set forth in one or more Orders (defined below). ## 1. Definitions. diff --git a/apps/www/data/legal/enterprise-terms/v2.mdx b/apps/www/data/legal/enterprise-terms/v2.mdx index b4824d2ae44..b12c8af2104 100644 --- a/apps/www/data/legal/enterprise-terms/v2.mdx +++ b/apps/www/data/legal/enterprise-terms/v2.mdx @@ -1,5 +1,3 @@ -_Last Modified: 1 May 2026_ - This Enterprise SaaS Subscription Agreement (this "**Agreement**") is entered into by and between the customer identified in the signature block below ("**Customer**") and Supabase, Pte. Ltd., as subsidiary of Supabase, Inc., a Delaware corporation ("**Supabase**") (Customer and Supabase each, a "**Party**" and collectively, the "**Parties**"), effective as of [Deal.CloseDate] ("**Effective Date**"), and sets forth the terms and conditions under which Customer may subscribe to certain products and services of Supabase as set forth in one or more Orders (defined below). **Accepted and agreed to as of the Effective Date by the authorized representative of each Party by Customer's entering into an Order referencing this Agreement.** diff --git a/apps/www/data/legal/enterprise-terms/v3.mdx b/apps/www/data/legal/enterprise-terms/v3.mdx index c156a7e28cb..538c559648c 100644 --- a/apps/www/data/legal/enterprise-terms/v3.mdx +++ b/apps/www/data/legal/enterprise-terms/v3.mdx @@ -1,5 +1,3 @@ -_Last Modified: 1 August 2026_ - This Enterprise SaaS Subscription Agreement (this "**Agreement**") is entered into by and between the customer identified in the signature block in an Order ("**Customer**") and Supabase, Pte. Ltd., as subsidiary of Supabase, Inc., a Delaware corporation ("**Supabase**") (Customer and Supabase each, a "**Party**" and collectively, the "**Parties**"), effective as of the date of last signature of an Order referencing these terms ("**Effective Date**"), and sets forth the terms and conditions under which Customer may subscribe to certain products and services of Supabase as set forth in one or more Orders (defined below). **Accepted and agreed to as of the Effective Date by the authorized representative of each Party by Customer's entering into an Order referencing this Agreement.** diff --git a/apps/www/pages/privacy-250528.mdx b/apps/www/data/legal/privacy/v1.mdx similarity index 99% rename from apps/www/pages/privacy-250528.mdx rename to apps/www/data/legal/privacy/v1.mdx index fca8be6f97c..62f973893f7 100644 --- a/apps/www/pages/privacy-250528.mdx +++ b/apps/www/data/legal/privacy/v1.mdx @@ -1,20 +1,5 @@ -import Layout from '../layouts/Layout' -import SectionContainer from '../components/Layouts/SectionContainer' import { PrivacySettings } from 'ui-patterns/PrivacySettings' -export const meta = { - title: 'Privacy Policy | Supabase', - description: 'Supabase Privacy Policy', -} - - - -# Privacy Policy - -_Last Modified: 28 May 2025_ - -Previous Version: [14 March 2025](/privacy-250314) - Thank you for your interest in Supabase, Inc., ("**_Supabase_**," "**_we_**", "**_our_**" or "**_us_**"). Supabase provides a suite of open source tools, stitched together to build a seamless developer experience. This Privacy Notice explains how information about you, that directly identifies you, or that makes you identifiable ("**_personal information_**") is collected, used and disclosed by Supabase in connection with our website at [supabase.com](https://supabase.com) (the "**_Site_**") and our services offered in connection with the Site (collectively with the Site, the "**_Service_**"). ## What Does This Privacy Notice Apply To? @@ -343,7 +328,3 @@ Most browsers also allow you to change your cookie settings to block certain coo If you would like to find out more about cookies and other similar technologies, please visit [allaboutcookies.org](http://www.allaboutcookies.org). Please note that deleting or blocking cookies may not be effective for all types of tracking technologies, such as Local Storage Objects (LSOs) like HTML5. - - - -export default (context) => diff --git a/apps/www/pages/privacy-260316.mdx b/apps/www/data/legal/privacy/v2.mdx similarity index 99% rename from apps/www/pages/privacy-260316.mdx rename to apps/www/data/legal/privacy/v2.mdx index e74d25cde71..a4f433b0293 100644 --- a/apps/www/pages/privacy-260316.mdx +++ b/apps/www/data/legal/privacy/v2.mdx @@ -1,20 +1,5 @@ -import Layout from '../layouts/Layout' -import SectionContainer from '../components/Layouts/SectionContainer' import { PrivacySettings } from 'ui-patterns/PrivacySettings' -export const meta = { - title: 'Privacy Policy | Supabase', - description: 'Supabase Privacy Policy', -} - - - -# Privacy Policy - -_Last Modified: 16 March 2026_ - -Previous Version: [28 May 2025](/privacy-250528) - Thank you for your interest in Supabase, Inc., ("**_Supabase_**," "**_we_**", "**_our_**" or "**_us_**"). Supabase provides a suite of open source tools, stitched together to build a seamless developer experience. This Privacy Notice explains how information about you, that directly identifies you, or that makes you identifiable ("**_personal information_**") is collected, used and disclosed by Supabase in connection with our website at [supabase.com](https://supabase.com) (the "**_Site_**") and our services offered in connection with the Site (collectively with the Site, the "**_Service_**"). ## What Does This Privacy Notice Apply To? @@ -346,7 +331,3 @@ Most browsers also allow you to change your cookie settings to block certain coo If you would like to find out more about cookies and other similar technologies, please visit [allaboutcookies.org](http://www.allaboutcookies.org). Please note that deleting or blocking cookies may not be effective for all types of tracking technologies, such as Local Storage Objects (LSOs) like HTML5. - - - -export default (context) => diff --git a/apps/www/pages/privacy.mdx b/apps/www/data/legal/privacy/v3.mdx similarity index 99% rename from apps/www/pages/privacy.mdx rename to apps/www/data/legal/privacy/v3.mdx index a462e70b58c..6e4fcc79f8d 100644 --- a/apps/www/pages/privacy.mdx +++ b/apps/www/data/legal/privacy/v3.mdx @@ -1,21 +1,5 @@ import { PrivacySettings } from 'ui-patterns/PrivacySettings' -import SectionContainer from '../components/Layouts/SectionContainer' -import Layout from '../layouts/Layout' - -export const meta = { - title: 'Privacy Policy | Supabase', - description: 'Supabase Privacy Policy', -} - - - -# Privacy Policy - -_Last Modified: 13 May 2026_ - -Previous Version: [16 March 2026](/privacy-260316) - Thank you for your interest in Supabase, Inc., ("**_Supabase_**," "**_we_**", "**_our_**" or "**_us_**"). Supabase provides a suite of open source tools, stitched together to build a seamless developer experience. This Privacy Notice explains how information about you, that directly identifies you, or that makes you identifiable ("**_personal information_**") is collected, used and disclosed by Supabase in connection with our website at [supabase.com](https://supabase.com) (the "**_Site_**") and our services offered in connection with the Site (collectively with the Site, the "**_Service_**"). ## What Does This Privacy Notice Apply To? @@ -349,7 +333,3 @@ Most browsers also allow you to change your cookie settings to block certain coo If you would like to find out more about cookies and other similar technologies, please visit [allaboutcookies.org](http://www.allaboutcookies.org). Please note that deleting or blocking cookies may not be effective for all types of tracking technologies, such as Local Storage Objects (LSOs) like HTML5. - - - -export default (context) => diff --git a/apps/www/pages/sla.mdx b/apps/www/data/legal/sla/v1.mdx similarity index 97% rename from apps/www/pages/sla.mdx rename to apps/www/data/legal/sla/v1.mdx index e69876d16ce..7a1cbb01058 100644 --- a/apps/www/pages/sla.mdx +++ b/apps/www/data/legal/sla/v1.mdx @@ -1,15 +1,3 @@ -import Layout from '../layouts/Layout' -import SectionContainer from '../components/Layouts/SectionContainer' - -export const meta = { - title: 'Service Level Agreement | Supabase', - description: 'Supabase Service Level Agreement', -} - - - -# Service Level Agreements - ## Enterprise Platform Uptime SLA The following Service Level Agreement, which is incorporated into and forms part of the Subscription Agreement between Supabase, Inc. ("Supabase") and Customer (the "Agreement"), will apply to the Services for Enterprise Customers specified in an Order Form during the applicable Subscription Term. @@ -370,7 +358,3 @@ An Information request about Supabase or feature request. | 4. Low | 2 business days
    Monday - Friday | 2 business days
    Monday - Friday | 24 hours
    24/7 x 365 | Business hours are 6am to 6pm local time unless stated otherwise. - -
    - -export default (context) => diff --git a/apps/www/pages/support-policy.mdx b/apps/www/data/legal/support-policy/v1.mdx similarity index 92% rename from apps/www/pages/support-policy.mdx rename to apps/www/data/legal/support-policy/v1.mdx index 9decc3cba6a..95a71a9cdfe 100644 --- a/apps/www/pages/support-policy.mdx +++ b/apps/www/data/legal/support-policy/v1.mdx @@ -1,15 +1,3 @@ -import SectionContainer from '../components/Layouts/SectionContainer' -import Layout from '../layouts/Layout' - -export const meta = { - title: 'Support Policy', - description: 'Supabase Support Policy', -} - - - -# Support Policy - Supabase is dedicated to providing an exceptional customer experience. As a crucial aspect of this commitment, we offer limited technical support for all paid projects running on our hosted Supabase platform. Technical support is confined to the scope, hours, contacts, and channels below. @@ -77,7 +65,3 @@ In the event of a platform issue, a notice will be posted on our platform status Please contact us to find out about our Premium Support options available for Team Plan and Enterprise customers using the form below: [https://forms.supabase.com/enterprise](https://forms.supabase.com/enterprise) - - - -export default (context) => diff --git a/apps/www/data/legal/terms/v1.mdx b/apps/www/data/legal/terms/v1.mdx index 328f7cbdd49..8205b0cd15f 100644 --- a/apps/www/data/legal/terms/v1.mdx +++ b/apps/www/data/legal/terms/v1.mdx @@ -1,5 +1,3 @@ -_Last Modified: 11 July 2025_ - These Terms of Service (this "**Agreement**") are a binding contract between you ("**Customer**," "**you**," or "**your**") and Supabase, Inc., a Delaware corporation with offices located at 65 Chulia Street #38-02/03, OCBC Centre, Singapore 049513 ("**Supabase**," "**we**," or "**us**"). This Agreement governs your access to and use of the Cloud Services. Supabase and Customer may be referred to herein collectively as the "**Parties**" or individually as a "**Party**." ## Agreement Acceptance diff --git a/apps/www/data/legal/terms/v2.mdx b/apps/www/data/legal/terms/v2.mdx index a790bafb29d..954a97df5ed 100644 --- a/apps/www/data/legal/terms/v2.mdx +++ b/apps/www/data/legal/terms/v2.mdx @@ -1,5 +1,3 @@ -_Last Modified: 1 May 2026_ - These Terms of Service (this "**Agreement**") are a binding contract between you ("**Customer**," "**you**," or "**your**") and SUPABASE PTE. LTD., a Singapore entity with a registered address of 65 Chulia Street #38-02/03, OCBC Centre, Singapore 049513 ("**Supabase**," "**we**," or "**us**"). This Agreement governs your access to and use of the Cloud Services. Supabase and Customer may be referred to herein collectively as the "**Parties**" or individually as a "**Party**." THIS AGREEMENT TAKES EFFECT WHEN YOU CLICK THE "I ACCEPT" BUTTON BELOW OR BY ACCESSING OR USING THE SERVICES (the "**Effective Date**"). BY CLICKING ON THE "I ACCEPT" BUTTON BELOW OR BY ACCESSING OR USING THE SERVICES YOU (A) ACKNOWLEDGE THAT YOU HAVE READ AND UNDERSTAND THIS AGREEMENT; (B) REPRESENT AND WARRANT THAT YOU HAVE THE RIGHT, POWER, AND AUTHORITY TO ENTER INTO THIS AGREEMENT AND, IF ENTERING INTO THIS AGREEMENT FOR AN ORGANIZATION, THAT YOU HAVE THE LEGAL AUTHORITY TO BIND THAT ORGANIZATION; AND (C) ACCEPT THIS AGREEMENT AND AGREE THAT YOU ARE LEGALLY BOUND BY ITS TERMS. diff --git a/apps/www/data/legal/terms/v3.mdx b/apps/www/data/legal/terms/v3.mdx index 6ac34f24b22..a5faf26fa39 100644 --- a/apps/www/data/legal/terms/v3.mdx +++ b/apps/www/data/legal/terms/v3.mdx @@ -1,5 +1,3 @@ -_Last Modified: 1 August 2026_ - These Terms of Service (this "Agreement") are a binding contract between you ("Customer," "you," or "your") and SUPABASE PTE. LTD., a Singapore entity with a registered address of 65 Chulia Street #38-02/03, OCBC Centre, Singapore 049513 ("Supabase," "we," or "us"). This Agreement governs your access to and use of the Cloud Services, except to the extent that Customer has entered into an Order or separate agreement with Supabase that expressly incorporates different terms, in which case those other terms will govern. Supabase and Customer may be referred to herein collectively as the "Parties" or individually as a "Party." THIS AGREEMENT TAKES EFFECT WHEN YOU CLICK THE "I ACCEPT" BUTTON BELOW OR BY ACCESSING OR USING THE SERVICES (the "Effective Date"). BY CLICKING ON THE "I ACCEPT" BUTTON BELOW OR BY ACCESSING OR USING THE SERVICES YOU (A) ACKNOWLEDGE THAT YOU HAVE READ AND UNDERSTAND THIS AGREEMENT; (B) REPRESENT AND WARRANT THAT YOU HAVE THE RIGHT, POWER, AND AUTHORITY TO ENTER INTO THIS AGREEMENT AND, IF ENTERING INTO THIS AGREEMENT FOR AN ORGANIZATION, THAT YOU HAVE THE LEGAL AUTHORITY TO BIND THAT ORGANIZATION; AND (C) ACCEPT THIS AGREEMENT AND AGREE THAT YOU ARE LEGALLY BOUND BY ITS TERMS. diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index b4e6ba6618a..ccff21267ae 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -1732,6 +1732,16 @@ module.exports = [ source: '/docs/company/privacy', destination: '/privacy', }, + { + permanent: true, + source: '/privacy-260316', + destination: '/privacy?version=v2', + }, + { + permanent: true, + source: '/privacy-250528', + destination: '/privacy?version=v1', + }, { permanent: true, source: '/docs/company/sla', diff --git a/apps/www/pages/aup.tsx b/apps/www/pages/aup.tsx new file mode 100644 index 00000000000..2c779f30437 --- /dev/null +++ b/apps/www/pages/aup.tsx @@ -0,0 +1,35 @@ +import { MDXProvider } from '@mdx-js/react' +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import LegalDocVersions, { type LegalDocVersion } from '~/components/Legal/LegalDocVersions' +import PageBreadcrumb from '~/components/Sections/PageBreadcrumb' +import PageHeader from '~/components/Sections/PageHeader' +import V1 from '~/data/legal/aup/v1.mdx' +import mdxComponents from '~/lib/mdx/mdxComponents' +import { NextSeo } from 'next-seo' + +const meta = { + title: 'Acceptable Use Policy | Supabase', + description: 'Supabase Acceptable Use Policy', +} + +const versions: LegalDocVersion[] = [ + { id: 'v1', label: 'Version 1', effectiveDate: 'June 1, 2026', Component: V1 }, +] + +export default function AcceptableUsePolicyPage() { + return ( + + + } + h1="Acceptable Use Policy" + /> + + + + + + + ) +} diff --git a/apps/www/pages/enterprise-terms.tsx b/apps/www/pages/enterprise-terms.tsx index be4507aeef0..9dff0a4d440 100644 --- a/apps/www/pages/enterprise-terms.tsx +++ b/apps/www/pages/enterprise-terms.tsx @@ -2,6 +2,8 @@ import { MDXProvider } from '@mdx-js/react' import DefaultLayout from '~/components/Layouts/Default' import SectionContainer from '~/components/Layouts/SectionContainer' import LegalDocVersions, { type LegalDocVersion } from '~/components/Legal/LegalDocVersions' +import PageBreadcrumb from '~/components/Sections/PageBreadcrumb' +import PageHeader from '~/components/Sections/PageHeader' import V1 from '~/data/legal/enterprise-terms/v1.mdx' import V2 from '~/data/legal/enterprise-terms/v2.mdx' import V3 from '~/data/legal/enterprise-terms/v3.mdx' @@ -25,13 +27,14 @@ export default function EnterpriseTermsPage() { return ( + } + h1="Enterprise SaaS Subscription Agreement" + /> -
    - -

    Enterprise SaaS Subscription Agreement

    - -
    -
    + + +
    ) diff --git a/apps/www/pages/legal/dpa.tsx b/apps/www/pages/legal/dpa.tsx index 1d2506851e0..51458b76b4c 100644 --- a/apps/www/pages/legal/dpa.tsx +++ b/apps/www/pages/legal/dpa.tsx @@ -1,60 +1,71 @@ -import Layout from '~/components/Layouts/Default' +import DefaultLayout from '~/components/Layouts/Default' import SectionContainer from '~/components/Layouts/SectionContainer' +import PageBreadcrumb from '~/components/Sections/PageBreadcrumb' +import PageHeader from '~/components/Sections/PageHeader' import { useSendTelemetryEvent } from '~/lib/telemetry' import CTABanner from 'components/CTABanner/index' +import { NextSeo } from 'next-seo' + +const meta = { + title: 'Data Processing Addendum', + description: 'Supabase Data Processing Addendum', +} const DPA = () => { const sendTelemetryEvent = useSendTelemetryEvent() return ( - <> - - -
    -
    -
    -

    DPA

    -

    - We have a long-standing commitment to customer privacy and data protection. As - part of this commitment, we have prepared a Data Processing Addendum ("DPA"). You - can review a static PDF version of our latest DPA document{' '} - - sendTelemetryEvent({ - action: 'dpa_pdf_opened', - properties: { source: 'www' }, - }) - } - > - here - - . -

    + + + + } + h1="Data Processing Addendum" + /> + +

    + We have a long-standing commitment to customer privacy and data protection. As part of + this commitment, we have prepared a Data Processing Addendum ("DPA"). You can review a + static PDF version of our latest DPA document{' '} + + sendTelemetryEvent({ + action: 'dpa_pdf_opened', + properties: { source: 'www' }, + }) + } + > + here + + . +

    -

    - To make the DPA legally binding, you need to sign and complete the details through - a PandaDoc document that we prepare. To get this version of the DPA,{' '} - - request it from the legal documents page - {' '} - of your Supabase dashboard. -

    -
    -
    -
    -
    - -
    - +

    + To make the DPA legally binding, you need to sign and complete the details through a + PandaDoc document that we prepare. To get this version of the DPA,{' '} + + request it from the legal documents page + {' '} + of your Supabase dashboard. +

    + + +
    ) } export default DPA diff --git a/apps/www/pages/privacy.tsx b/apps/www/pages/privacy.tsx new file mode 100644 index 00000000000..11940a8c612 --- /dev/null +++ b/apps/www/pages/privacy.tsx @@ -0,0 +1,39 @@ +import { MDXProvider } from '@mdx-js/react' +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import LegalDocVersions, { type LegalDocVersion } from '~/components/Legal/LegalDocVersions' +import PageBreadcrumb from '~/components/Sections/PageBreadcrumb' +import PageHeader from '~/components/Sections/PageHeader' +import V1 from '~/data/legal/privacy/v1.mdx' +import V2 from '~/data/legal/privacy/v2.mdx' +import V3 from '~/data/legal/privacy/v3.mdx' +import mdxComponents from '~/lib/mdx/mdxComponents' +import { NextSeo } from 'next-seo' + +const meta = { + title: 'Privacy Policy | Supabase', + description: 'Supabase Privacy Policy', +} + +const versions: LegalDocVersion[] = [ + { id: 'v3', label: 'Version 3', effectiveDate: 'May 13, 2026', Component: V3 }, + { id: 'v2', label: 'Version 2', effectiveDate: 'March 16, 2026', Component: V2 }, + { id: 'v1', label: 'Version 1', effectiveDate: 'May 28, 2025', Component: V1 }, +] + +export default function PrivacyPolicyPage() { + return ( + + + } + h1="Privacy Policy" + /> + + + + + + + ) +} diff --git a/apps/www/pages/sla.tsx b/apps/www/pages/sla.tsx new file mode 100644 index 00000000000..c1fe411c717 --- /dev/null +++ b/apps/www/pages/sla.tsx @@ -0,0 +1,37 @@ +import { MDXProvider } from '@mdx-js/react' +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import PageBreadcrumb from '~/components/Sections/PageBreadcrumb' +import PageHeader from '~/components/Sections/PageHeader' +import Content from '~/data/legal/sla/v1.mdx' +import mdxComponents from '~/lib/mdx/mdxComponents' +import { NextSeo } from 'next-seo' + +const meta = { + title: 'Service Level Agreement | Supabase', + description: 'Supabase Service Level Agreement', +} + +export default function SLAPage() { + return ( + + + + } + h1="Service Level Agreement" + /> + + + + + + + ) +} diff --git a/apps/www/pages/support-policy.tsx b/apps/www/pages/support-policy.tsx new file mode 100644 index 00000000000..7528e1f5b6f --- /dev/null +++ b/apps/www/pages/support-policy.tsx @@ -0,0 +1,37 @@ +import { MDXProvider } from '@mdx-js/react' +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import PageBreadcrumb from '~/components/Sections/PageBreadcrumb' +import PageHeader from '~/components/Sections/PageHeader' +import Content from '~/data/legal/support-policy/v1.mdx' +import mdxComponents from '~/lib/mdx/mdxComponents' +import { NextSeo } from 'next-seo' + +const meta = { + title: 'Support Policy', + description: 'Supabase Support Policy', +} + +export default function SupportPolicyPage() { + return ( + + + + } + h1="Support Policy" + /> + + + + + + + ) +} diff --git a/apps/www/pages/terms.tsx b/apps/www/pages/terms.tsx index a1f603dd6bc..36f4c77338e 100644 --- a/apps/www/pages/terms.tsx +++ b/apps/www/pages/terms.tsx @@ -2,6 +2,8 @@ import { MDXProvider } from '@mdx-js/react' import DefaultLayout from '~/components/Layouts/Default' import SectionContainer from '~/components/Layouts/SectionContainer' import LegalDocVersions, { type LegalDocVersion } from '~/components/Legal/LegalDocVersions' +import PageBreadcrumb from '~/components/Sections/PageBreadcrumb' +import PageHeader from '~/components/Sections/PageHeader' import V1 from '~/data/legal/terms/v1.mdx' import V2 from '~/data/legal/terms/v2.mdx' import V3 from '~/data/legal/terms/v3.mdx' @@ -23,13 +25,21 @@ export default function TermsPage() { return ( + + } + h1="Terms of Service" + /> -
    - -

    Terms of service

    - -
    -
    + + +
    ) From 91b40f15abc715db5825ba788b8497749497439f Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:22:10 +0100 Subject: [PATCH 122/141] docs: how to fully remove account access when deleting a user (#48487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The [User Management → Deleting users](https://supabase.com/docs/guides/auth/managing-user-data) section warned that deleting a user does not sign them out, but did not say what to do about it. Adds a **Removing account access** subsection: revoke sessions before deleting, why a soft-delete flag or [ban](https://supabase.com/docs/reference/javascript/auth-admin-updateuserbyid) is not a substitute, and the residual [access-token window](https://supabase.com/docs/guides/auth/sessions) after revocation. Fills a docs gap surfaced by [supabase/agent-skills#194](https://github.com/supabase/agent-skills/pull/194) while investigating the [`investigate-auth-001-deleted-user-access`](https://github.com/supabase/evals/blob/main/evals/investigate-auth-001-deleted-user-access/PROMPT.md) eval scenario. ## Summary by CodeRabbit * **Documentation** * Updated the “Deleting users” guidance to specify deleting via `auth.admin.deleteUser()` (with `shouldSoftDelete: false`) and clarify that this cascades to sessions, invalidates refresh tokens, and blocks new access-token minting. * Rewrote the explanation to emphasize that it does not substitute for temporary bans or application-level “deleted” states. * Clarified the access-token window: already-issued stateless JWTs remain valid until `exp`, and recommended mitigations include short JWT expiry and enforcing session validation (via `session_id`) for sensitive actions. --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../content/guides/auth/managing-user-data.mdx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/docs/content/guides/auth/managing-user-data.mdx b/apps/docs/content/guides/auth/managing-user-data.mdx index 8e1dffb4ca5..d703563143c 100644 --- a/apps/docs/content/guides/auth/managing-user-data.mdx +++ b/apps/docs/content/guides/auth/managing-user-data.mdx @@ -210,6 +210,22 @@ You will encounter an error when you try to delete an Auth user that owns any St
    +### Removing account access + +When the goal is to remove an account so it can no longer access your app, delete the auth user with [`auth.admin.deleteUser()`](/docs/reference/javascript/auth-admin-deleteuser). With the default `shouldSoftDelete: false`, this removes the row from `auth.users`, which cascades to `auth.sessions` and invalidates the user's refresh tokens — so the account can no longer mint new access tokens. + +A few things are _not_ a substitute for deleting the user: + +- A temporary [ban](/docs/reference/javascript/auth-admin-updateuserbyid) only blocks sign-in for its duration and does not revoke existing sessions. +- Marking the account as deleted only in your own application tables leaves the `auth.users` row intact, so it can still authenticate and refresh. + +Deleting the user still cannot retroactively invalidate an access token that was already issued. Supabase access tokens are stateless JWTs, so a token already in the user's hands stays valid until its `exp` claim passes, and during that window the account can still call the API. You have two ways to handle this window: + +- **Bound it:** keep the [access token (JWT) expiry](/docs/guides/auth/sessions) short, so any outstanding token expires soon after you delete the user. +- **Close it for sensitive operations:** validate the `session_id` claim against the `auth.sessions` table on those operations. Because deleting the user removes the session row, an outstanding token fails this check — but only on the requests where you perform it; other API calls still accept the token until `exp`. + +See [User sessions](/docs/guides/auth/sessions) for details. + ## Exporting users As Supabase is built on top of Postgres, you can query the `auth.users` and `auth.identities` table via the `SQL Editor` tab to extract all users: From 6b14df77246004089823ec9b375cfe5387be4ea7 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 30 Jul 2026 14:23:43 +0200 Subject: [PATCH 123/141] chore: Bump vulnerable deps (#48387) ## Summary by CodeRabbit * **Chores** * Updated Next.js, PostCSS, and tar package versions. * Added the required TypeScript native tooling where needed. * Refined package configuration and dependency ordering across the project. * Removed an unused empty dependency configuration. --- apps/design-system/package.json | 4 +- apps/docs/package.json | 6 +- apps/studio/package.json | 6 +- apps/ui-library/package.json | 2 +- apps/www/package.json | 4 +- blocks/vue/package.json | 2 +- packages/common/package.json | 4 +- packages/generator/package.json | 1 - packages/ui-patterns/package.json | 2 +- packages/ui/package.json | 2 +- pnpm-lock.yaml | 704 +++++++++++++++--------------- pnpm-workspace.yaml | 12 +- 12 files changed, 378 insertions(+), 371 deletions(-) diff --git a/apps/design-system/package.json b/apps/design-system/package.json index d1ffddf6503..8b8da975e36 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -19,8 +19,8 @@ "dependencies": { "@hookform/resolvers": "^3.1.1", "@tanstack/react-table": "catalog:", - "contentlayer2": "0.4.6", "common": "workspace:*", + "contentlayer2": "0.4.6", "date-fns": "^2.30.0", "dayjs": "1.11.13", "eslint-config-supabase": "workspace:*", @@ -57,6 +57,7 @@ "@types/lodash.template": "4.5.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", + "@typescript/native": "catalog:", "config": "workspace:*", "mdast-util-toc": "^6.1.1", "npm-run-all": "^4.1.5", @@ -66,7 +67,6 @@ "tailwindcss": "catalog:", "tsconfig": "workspace:*", "tsx": "catalog:", - "@typescript/native": "catalog:", "typescript": "catalog:", "unist-builder": "3.0.0" } diff --git a/apps/docs/package.json b/apps/docs/package.json index e343ae6e65c..a016e79a1b0 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -102,7 +102,7 @@ "mdast-util-to-string": "^3.1.1", "micromark-extension-gfm": "^2.0.3", "micromark-extension-mdxjs": "^1.0.0", - "next": "^15.5.18", + "next": "^15.5.21", "next-mdx-remote-client": "^1.1.7", "next-plugin-yaml": "^1.0.1", "next-themes": "catalog:", @@ -157,6 +157,7 @@ "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "catalog:", "@types/unist": "^2.0.6", + "@typescript/native": "catalog:", "amaro": "^1.1.5", "api-types": "workspace:*", "cheerio": "^1.0.0-rc.12", @@ -175,11 +176,10 @@ "slugify": "^1.6.6", "smol-toml": "^1.3.1", "tailwindcss": "catalog:", - "tar": "^7.5.19", + "tar": "^7.5.21", "tsconfig": "workspace:*", "tsx": "catalog:", "twoslash": "^0.3.1", - "@typescript/native": "catalog:", "typescript": "catalog:", "unist-util-visit-parents": "5.1.3", "vite": "catalog:", diff --git a/apps/studio/package.json b/apps/studio/package.json index 8bc27056365..2f3d947c9ed 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -69,6 +69,7 @@ "@std/path": "npm:@jsr/std__path@^1.0.8", "@stripe/react-stripe-js": "6.1.0", "@stripe/stripe-js": "9.1.0", + "@stripe/sync-engine": "1.0.32", "@supabase/auth-js": "catalog:", "@supabase/mcp-server-supabase": "^0.7.0", "@supabase/pg-meta": "workspace:*", @@ -150,7 +151,6 @@ "sonner": "^1.5.0", "sql-formatter": "^15.0.0", "streamdown": "^1.3.0", - "@stripe/sync-engine": "1.0.32", "swiper": "^12.1.2", "tus-js-client": "^4.1.0", "ui": "workspace:*", @@ -168,6 +168,7 @@ "@faker-js/faker": "^9.9.0", "@graphql-codegen/cli": "5.0.5", "@graphql-typed-document-node/core": "^3.2.0", + "@jridgewell/remapping": "^2.3.5", "@tailwindcss/vite": "4.2.4", "@tanstack/devtools-vite": "^0.6.0", "@testing-library/dom": "^10.0.0", @@ -188,7 +189,7 @@ "@types/react-grid-layout": "^1.3.0", "@types/react-simple-maps": "^3.0.1", "@types/zxcvbn": "^4.4.1", - "@jridgewell/remapping": "^2.3.5", + "@typescript/native": "catalog:", "@vercel/config": "^0.2.1", "@vitejs/plugin-react": "catalog:", "@vitest/coverage-v8": "catalog:", @@ -215,7 +216,6 @@ "require-in-the-middle": "^8.0.0", "tsconfig": "workspace:*", "tsx": "catalog:", - "@typescript/native": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vite-tsconfig-paths": "catalog:", diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index dfbcb6c3de8..62abc5c3b9e 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -80,6 +80,7 @@ "@types/lodash": "^4.17.16", "@types/react": "catalog:", "@types/react-dom": "catalog:", + "@typescript/native": "catalog:", "config": "workspace:^", "lodash": "catalog:", "mdast-util-toc": "^6.1.1", @@ -92,7 +93,6 @@ "tailwindcss": "catalog:", "tsconfig": "workspace:*", "tsx": "catalog:", - "@typescript/native": "catalog:", "typescript": "catalog:", "vite": "catalog:" } diff --git a/apps/www/package.json b/apps/www/package.json index 901f465841e..c67c4195a47 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -38,6 +38,7 @@ "@sentry/nextjs": "catalog:", "@supabase/ssr": "catalog:", "@supabase/supabase-js": "catalog:", + "@typescript/native": "catalog:", "@vercel/og": "^0.6.2", "ai-commands": "workspace:*", "animejs": "^4.0.2", @@ -66,7 +67,7 @@ "mdast-util-to-markdown": "^1.5.0", "micromark-extension-gfm": "^2.0.3", "micromark-extension-mdxjs": "^1.0.1", - "next": "^15.5.18", + "next": "^15.5.21", "next-mdx-remote-client": "^1.1.7", "next-seo": "^6.5.0", "next-themes": "catalog:", @@ -91,7 +92,6 @@ "shiki": "^4.2.0", "swiper": "^12.1.2", "typed.js": "^2.0.16", - "@typescript/native": "catalog:", "typescript": "catalog:", "ui": "workspace:*", "ui-patterns": "workspace:*", diff --git a/blocks/vue/package.json b/blocks/vue/package.json index 39fd1c19c31..4ca3008fd4c 100644 --- a/blocks/vue/package.json +++ b/blocks/vue/package.json @@ -24,9 +24,9 @@ "vue-router": "^4.5.1" }, "devDependencies": { + "@typescript/native": "catalog:", "shadcn": "^3.3.1", "tsconfig": "workspace:*", - "@typescript/native": "catalog:", "typescript": "catalog:", "vite": "^7.3.2" } diff --git a/packages/common/package.json b/packages/common/package.json index bc9c7ebd64d..b874061734e 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -26,16 +26,16 @@ "valtio": "catalog:" }, "devDependencies": { - "lucide-react": "*", "@types/lodash": "4.17.5", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", + "@typescript/native": "catalog:", "@vitest/coverage-v8": "catalog:", "@vitest/ui": "catalog:", + "lucide-react": "*", "tsconfig": "workspace:*", "type-fest": "5.6.0", - "@typescript/native": "catalog:", "typescript": "catalog:", "vitest": "catalog:" }, diff --git a/packages/generator/package.json b/packages/generator/package.json index cb3e56a7e71..0d81708b40e 100644 --- a/packages/generator/package.json +++ b/packages/generator/package.json @@ -20,7 +20,6 @@ "tsdoc:dereference:storage:v2": "tsx ./tsdoc.ts --input ../../apps/docs/spec/enrichments/tsdoc_v2/storage.json --output ../../apps/docs/spec/enrichments/tsdoc_v2/storage_dereferenced.json", "tsdoc:dereference:supabase:v2": "tsx ./tsdoc.ts --input ../../apps/docs/spec/enrichments/tsdoc_v2/supabase.json --output ../../apps/docs/spec/enrichments/tsdoc_v2/supabase_dereferenced.json" }, - "dependencies": {}, "devDependencies": { "@types/json-stringify-safe": "^5.0.0", "@types/lodash": "^4.14.202", diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 749f48a721c..1955a7f3195 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -815,13 +815,13 @@ "@types/react": "catalog:", "@types/react-dom": "catalog:", "@types/react-syntax-highlighter": "^15.5.13", + "@typescript/native": "catalog:", "@vitest/coverage-v8": "catalog:", "api-types": "workspace:*", "config": "workspace:*", "next-router-mock": "^0.9.13", "tailwindcss": "^4.2.4", "tsx": "catalog:", - "@typescript/native": "catalog:", "typescript": "catalog:", "unified": "^11.0.5", "vfile": "^6.0.3", diff --git a/packages/ui/package.json b/packages/ui/package.json index b9fb78d53c2..f87bfb5a4a5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -52,11 +52,11 @@ "@types/react": "catalog:", "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "catalog:", + "@typescript/native": "catalog:", "@vitest/coverage-v8": "catalog:", "config": "workspace:*", "tsconfig": "workspace:*", "tsx": "catalog:", - "@typescript/native": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vitest": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b64b52b7c22..12e2b783928 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,8 +61,8 @@ catalogs: specifier: ^4.1.4 version: 4.1.4 next: - specifier: 16.2.6 - version: 16.2.6 + specifier: ^16.2.11 + version: 16.2.11 next-themes: specifier: ^0.4.6 version: 0.4.6 @@ -106,24 +106,24 @@ catalogs: overrides: '@ardatan/relay-compiler>immutable': ^3.8.3 monaco-editor: 0.52.2 - '@mapbox/node-pre-gyp>tar': ^7.5.19 + '@mapbox/node-pre-gyp>tar': ^7.5.21 '@sentry/webpack-plugin>uuid': ^11.1.1 '@usercentrics/cmp-browser-sdk>uuid': ^11.1.1 braintrust>esbuild: ^0.28.1 braintrust>uuid: ^11.1.1 - cacache>tar: ^7.5.19 + cacache>tar: ^7.5.21 dompurify: ^3.3.2 express-rate-limit>ip-address: ^10.1.1 h3@1: 1.15.11 lodash: ^4.18.1 lodash-es: ^4.18.1 mdx-bundler>uuid: ^11.1.1 - node-gyp>tar: ^7.5.19 + node-gyp>tar: ^7.5.21 nodemailer: ^7.0.11 - postcss: ^8.5.10 + postcss: ^8.5.18 qs: ^6.15.2 refractor>prismjs: ^1.30.0 - supabase>tar: ^7.5.19 + supabase>tar: ^7.5.21 tmp: ^0.2.7 vite>esbuild: ^0.28.1 webpack: ^5.104.1 @@ -223,10 +223,10 @@ importers: version: 1.2.0 next: specifier: 'catalog:' - version: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + version: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -316,8 +316,8 @@ importers: specifier: ^4.1.5 version: 4.1.5 postcss: - specifier: ^8.5.10 - version: 8.5.10 + specifier: ^8.5.18 + version: 8.5.22 rimraf: specifier: ^4.1.3 version: 4.4.1 @@ -377,7 +377,7 @@ importers: version: 8.1.0(@octokit/core@7.0.6) '@sentry/nextjs': specifier: 'catalog:' - version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) + version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) '@supabase/supabase-js': specifier: 'catalog:' version: 2.110.9 @@ -490,8 +490,8 @@ importers: specifier: ^1.0.0 version: 1.0.1 next: - specifier: ^15.5.18 - version: 15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + specifier: ^15.5.21 + version: 15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-mdx-remote-client: specifier: ^1.1.7 version: 1.1.7(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(unified@11.0.5) @@ -503,7 +503,7 @@ importers: version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) nuqs: specifier: ^1.19.1 - version: 1.19.1(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4)) + version: 1.19.1(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4)) openai: specifier: ^4.75.1 version: 4.104.0(encoding@0.1.13)(ws@8.21.0)(zod@3.25.76) @@ -683,8 +683,8 @@ importers: specifier: ^12.1.3 version: 12.1.3 postcss: - specifier: ^8.5.10 - version: 8.5.10 + specifier: ^8.5.18 + version: 8.5.22 shiki: specifier: ^3.2.1 version: 3.13.0 @@ -704,8 +704,8 @@ importers: specifier: 'catalog:' version: 4.2.4 tar: - specifier: ^7.5.19 - version: 7.5.20 + specifier: ^7.5.21 + version: 7.5.21 tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -750,10 +750,10 @@ importers: version: 0.511.0(react@19.2.6) next: specifier: 'catalog:' - version: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + version: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -826,13 +826,13 @@ importers: dependencies: '@react-router/fs-routes': specifier: ^7.17.0 - version: 7.17.0(@react-router/dev@7.17.0(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2) + version: 7.17.0(@react-router/dev@7.17.0(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2) '@react-router/node': specifier: ^7.17.0 - version: 7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) + version: 7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) '@react-router/serve': specifier: ^7.17.0 - version: 7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2) + version: 7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2) isbot: specifier: ^5.1.36 version: 5.1.36 @@ -844,7 +844,7 @@ importers: version: 19.2.6(react@19.2.6) react-router: specifier: ^7.17.0 - version: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) ui: specifier: workspace:* version: link:../../packages/ui @@ -854,7 +854,7 @@ importers: devDependencies: '@react-router/dev': specifier: ^7.17.0 - version: 7.17.0(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) + version: 7.17.0(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) '@tailwindcss/vite': specifier: 4.2.4 version: 4.2.4(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -956,7 +956,7 @@ importers: version: 1.1.1(@types/react@19.2.14)(react@19.2.6) '@sentry/nextjs': specifier: 'catalog:' - version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) + version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) '@sentry/react': specifier: ^10.27.0 version: 10.59.0(react@19.2.6) @@ -1139,13 +1139,13 @@ importers: version: 0.52.2 next: specifier: 'catalog:' - version: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + version: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) nuqs: specifier: 2.7.1 - version: 2.7.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) + version: 2.7.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) openai: specifier: ^4.104.0 version: 4.104.0(encoding@0.1.13)(ws@8.21.0)(zod@3.25.76) @@ -1386,13 +1386,13 @@ importers: version: 2.11.3(@types/node@22.13.14)(typescript@6.0.2) next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6) + version: 0.9.13(next@16.2.11(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6) node-mocks-http: specifier: ^1.17.2 version: 1.17.2(@types/node@22.13.14) postcss: - specifier: ^8.5.10 - version: 8.5.10 + specifier: ^8.5.18 + version: 8.5.22 raw-loader: specifier: ^4.0.2 version: 4.0.2(webpack@5.105.4(esbuild@0.28.1)) @@ -1428,7 +1428,7 @@ importers: version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@react-router/fs-routes': specifier: ^7.4.0 - version: 7.4.0(@react-router/dev@7.13.2(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2) + version: 7.4.0(@react-router/dev@7.13.2(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2) '@supabase-labs/y-supabase': specifier: 0.1.0 version: 0.1.0 @@ -1488,10 +1488,10 @@ importers: version: 0.52.2 next: specifier: 'catalog:' - version: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + version: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-contentlayer2: specifier: 0.4.6 - version: 0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1) + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -1573,7 +1573,7 @@ importers: version: 7.29.7(supports-color@8.1.1) '@react-router/dev': specifier: ^7.9.0 - version: 7.13.2(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) + version: 7.13.2(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) '@shikijs/compat': specifier: ^1.1.7 version: 1.6.0 @@ -1614,14 +1614,14 @@ importers: specifier: ^6.1.1 version: 6.1.1 postcss: - specifier: ^8.5.10 - version: 8.5.10 + specifier: ^8.5.18 + version: 8.5.22 react-dropzone: specifier: ^14.3.8 version: 14.3.8(react@19.2.6) react-router: specifier: ^7.13.2 - version: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) rimraf: specifier: ^4.1.3 version: 4.4.1 @@ -1693,7 +1693,7 @@ importers: version: 21.1.1 '@sentry/nextjs': specifier: 'catalog:' - version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) + version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) '@supabase/ssr': specifier: 'catalog:' version: 0.10.2(@supabase/supabase-js@2.110.9) @@ -1788,20 +1788,20 @@ importers: specifier: ^1.0.1 version: 1.0.1 next: - specifier: ^15.5.18 - version: 15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + specifier: ^15.5.21 + version: 15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-mdx-remote-client: specifier: ^1.1.7 version: 1.1.7(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1)(unified@11.0.5) next-seo: specifier: ^6.5.0 - version: 6.5.0(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 6.5.0(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) nuqs: specifier: ^2.8.1 - version: 2.8.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) + version: 2.8.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6) ogl: specifier: ^1.0.11 version: 1.0.11 @@ -1873,7 +1873,7 @@ importers: version: link:../../packages/ui-patterns unicornstudio-react: specifier: 2.0.1-1 - version: 2.0.1-1(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 2.0.1-1(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) use-debounce: specifier: ^7.0.1 version: 7.0.1(react@19.2.6) @@ -1924,8 +1924,8 @@ importers: specifier: workspace:* version: link:../../packages/api-types postcss: - specifier: ^8.5.10 - version: 8.5.10 + specifier: ^8.5.18 + version: 8.5.22 react-hook-form: specifier: ^7.71.2 version: 7.72.1(react@19.2.6) @@ -2181,13 +2181,13 @@ importers: version: 0.7.9 flags: specifier: ^4.0.0 - version: 4.0.1(@opentelemetry/api@1.9.1)(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 4.0.1(@opentelemetry/api@1.9.1)(next@16.2.11(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) lodash: specifier: ^4.18.1 version: 4.18.1 next: specifier: 'catalog:' - version: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + version: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -2304,10 +2304,10 @@ importers: version: link:../config next: specifier: 'catalog:' - version: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + version: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6) + version: 0.9.13(next@16.2.11(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6) react: specifier: 'catalog:' version: 19.2.6 @@ -2689,7 +2689,7 @@ importers: version: 0.52.2 next: specifier: 'catalog:' - version: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + version: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) next-themes: specifier: 'catalog:' version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -2822,7 +2822,7 @@ importers: version: link:../config next-router-mock: specifier: ^0.9.13 - version: 0.9.13(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6) + version: 0.9.13(next@16.2.11(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6) tailwindcss: specifier: ^4.2.4 version: 4.2.4 @@ -4747,11 +4747,11 @@ packages: '@next/env@14.2.35': resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@next/env@15.5.18': - resolution: {integrity: sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==} + '@next/env@15.5.21': + resolution: {integrity: sha512-hjJI/GfrjWHgNguRIBzItjRRu0m3Nrz17GhxsjuHfjIvg9hyg3239REd2dpI+bpMTFuVrVprHzEQ19m++cDtbw==} - '@next/env@16.2.6': - resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} + '@next/env@16.2.11': + resolution: {integrity: sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==} '@next/eslint-plugin-next@15.5.4': resolution: {integrity: sha512-SR1vhXNNg16T4zffhJ4TS7Xn7eq4NfKfcOsRwea7RIAHrjRpI9ALYbamqIJqkAhowLlERffiwk0FMvTLNdnVtw==} @@ -4767,106 +4767,106 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@15.5.18': - resolution: {integrity: sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==} + '@next/swc-darwin-arm64@15.5.21': + resolution: {integrity: sha512-ZfjqPEdi6TRC/fWx7UDbwb1fbVgyh2uD5tVTRKIDZDlYM+UNuE/LafDG2fwuAoZilADpABh46OY/F5qf9JjqLQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@16.2.6': - resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} + '@next/swc-darwin-arm64@16.2.11': + resolution: {integrity: sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.18': - resolution: {integrity: sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==} + '@next/swc-darwin-x64@15.5.21': + resolution: {integrity: sha512-TlCf1NpxgQLzTrexuev75xwmNCJMd1/qkJpTVP1GRRcih93hlIBn1P72hkh8T0gnRFr6BmWksQtbyG3jT6jnww==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@16.2.6': - resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} + '@next/swc-darwin-x64@16.2.11': + resolution: {integrity: sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.18': - resolution: {integrity: sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==} + '@next/swc-linux-arm64-gnu@15.5.21': + resolution: {integrity: sha512-LXRsq1p+HvHSi7ygwNcSEEcK0zuo5jS75ZlqFHtOH+LF7qntXAJVJxah+1Pi/GyBm7EpkwU7m4EgbvIKrMqm9A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-gnu@16.2.6': - resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} + '@next/swc-linux-arm64-gnu@16.2.11': + resolution: {integrity: sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@15.5.18': - resolution: {integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==} + '@next/swc-linux-arm64-musl@15.5.21': + resolution: {integrity: sha512-hyGixhFxpDKjqoev6l4KlcRBlt9AXWrGhDZwmwg49sMJM5tnKQPSi+SEj9+e5n+l/bthRGZUdh59GKIs6lQPRw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-arm64-musl@16.2.6': - resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} + '@next/swc-linux-arm64-musl@16.2.11': + resolution: {integrity: sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@15.5.18': - resolution: {integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==} + '@next/swc-linux-x64-gnu@15.5.21': + resolution: {integrity: sha512-qfE+YfOba6S2+13e8qn1/UozDVNZ2clBlrs8UtDoax4s8ediu6sq93z66OEHUYlb69Tffh5JTNkgtsAKiSuugg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-gnu@16.2.6': - resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} + '@next/swc-linux-x64-gnu@16.2.11': + resolution: {integrity: sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@15.5.18': - resolution: {integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==} + '@next/swc-linux-x64-musl@15.5.21': + resolution: {integrity: sha512-BXLGG+EvIwp/Rrgl6HY8sqvD6BOUOIRz8/naDbeLNX7mlA5H2XRcL6MW/0IGnJISfj5BA9gNhFyJj5yOoiIDJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-linux-x64-musl@16.2.6': - resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} + '@next/swc-linux-x64-musl@16.2.11': + resolution: {integrity: sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@15.5.18': - resolution: {integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==} + '@next/swc-win32-arm64-msvc@15.5.21': + resolution: {integrity: sha512-tNGNOlT0Wn7E4IMsSnufjXN/l2L2/AGdLLpa2vzS89SYCBuihgLn3ngLsIrvndAnWo9nAkus+4gZHTI/Ijx9HA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@16.2.6': - resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} + '@next/swc-win32-arm64-msvc@16.2.11': + resolution: {integrity: sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.18': - resolution: {integrity: sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==} + '@next/swc-win32-x64-msvc@15.5.21': + resolution: {integrity: sha512-DmIdWmC9p4rdNIiQqo8ap0+Cnj6kKtTZnuSCxoYydSc8sgpDgAg9wFhxplunak9imLV0pTvc5WVCOHwm5eHLtQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.6': - resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} + '@next/swc-win32-x64-msvc@16.2.11': + resolution: {integrity: sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -9474,7 +9474,7 @@ packages: engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} @@ -9617,6 +9617,10 @@ packages: resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -10267,19 +10271,19 @@ packages: resolution: {integrity: sha512-OTdKeYMlvQ8KBgyej5ysktnWJoeyo7rGrVnm+bdpIHGvxhbTGPsOkB+7T1EdTuX00dGlQQb2UEbSPB1OpMXULw==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 cssnano-utils@6.0.0: resolution: {integrity: sha512-ztS9W/+uaDn+bkYmDhs+GdMveHJ3CL8IPNHpRqDUQXv5GJOTQAJjV1XUOInr9esLXSabQV1pLRZlJpyUwEqDyQ==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 cssnano@8.0.1: resolution: {integrity: sha512-oSiOnPQNNYjusTUlYJiE6xvFQG4don3N0QavaoV1BxIsC1zjvxOwikXlR7lG1EVmZNDDaJkHbQx1VRB8kaoMHA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} @@ -11361,8 +11365,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -12201,8 +12205,8 @@ packages: resolution: {integrity: sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==} engines: {node: '>=0.10.0'} - immutable@4.3.8: - resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==} + immutable@4.3.9: + resolution: {integrity: sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==} import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -13880,8 +13884,8 @@ packages: react: '*' react-dom: '*' - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -13950,8 +13954,8 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - next@15.5.18: - resolution: {integrity: sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==} + next@15.5.21: + resolution: {integrity: sha512-/TsdBtkWLhkl+NVL3Uqws2UphNd6IPzOtzSk1fHaf+0P7GQKLZDUytyhns/Ykbzdy9+YRjwG7ONvrHaaTDdFqQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -13971,8 +13975,8 @@ packages: sass: optional: true - next@16.2.6: - resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} + next@16.2.11: + resolution: {integrity: sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -14714,151 +14718,151 @@ packages: resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} engines: {node: ^18.12 || ^20.9 || >=22.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-colormin@8.0.0: resolution: {integrity: sha512-KKwMmsSgsmdYXqrjQeqL3tnuIFtctiR1GEMHdjNpDpz/TCRkkkok2mMcreK2zVV3l7POWOmAkR2xYHUpRUK1DA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-convert-values@8.0.0: resolution: {integrity: sha512-Ohtj3rNZWawTRePv5NCHTy8VJSdJ/G/uKuxcxJreOMichuqcT6uEl2TAnopVeJCJ/c13jaSqg7m63yFLM5zBsA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-discard-comments@8.0.0: resolution: {integrity: sha512-zGpvVLj2sbagEp+BTVETvAfkZdGVA6rALNujDK/WTIjdf1/rQOxOG8BBzkI8UQgnw8SkL6xffAfbtGMHFypadw==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-discard-duplicates@8.0.0: resolution: {integrity: sha512-zjRyYmNGI3PTipKBBtCgExlmZXQn49KvKoaiNnR2g+iXxeNk7GY5Js2ULtZXPrCYeqjPagrzKIBNcBocvXCR7g==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-discard-empty@8.0.0: resolution: {integrity: sha512-kxPJg6EqahbBvm+l7hpYYCtpsv8dlz7Tv6wJXUXZaeuY0WGS61DxfGdZR4uVB/Cx+yi3iOHQVSqpSHKMFaBg6Q==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-discard-overridden@8.0.0: resolution: {integrity: sha512-sW2OWH3l9p0FmBSVr228uztFseqroZxwgD7SGF0Ks0dRPDttSo3P8FK5ZBLtWBH2A5+chpB0J2fB/T8heKHLBw==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-merge-longhand@8.0.0: resolution: {integrity: sha512-YDmAmQ8H+ljfomVpSXvr9NA0GP01fraQJqjWBYoMVGg6rOT+PJLwPyeVo2ekn4WB4ZVSH5ddtK3DTRxbz6CFzg==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-merge-rules@8.0.0: resolution: {integrity: sha512-bgstL5mpi41dDpnYGDUcI3M814NWkCMcIWpwDqEHXkHg3BT7b4XRAfNEuwJncZOVn/67kVKvWzhfv/7xyrp2uQ==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-minify-font-values@8.0.0: resolution: {integrity: sha512-EnOHQEnSt6oH5NrL1DMFAQuwB2IOimFXTCzc9bKfUeH1jREbqIF5MAK4gQJQOC4mPUwJt4sWifAmNZ1qLu6j3Q==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-minify-gradients@8.0.0: resolution: {integrity: sha512-43iAnYIGk0ZjNx5X/rkIcHi6dhmu/vEjY0kqfUfxPuJRO+V7jx8uKIdcnL0dpfNoC5J9TSh3EtzLWbq0gpqnWA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-minify-params@8.0.0: resolution: {integrity: sha512-z7w4QO7G55l4vMUK1Lmx03GW7iyRLgf2V5Dz/7ioSPLnXRjeD+b7m0XfAXUGrbBYYrJ6bXPk+3LoX5u4JfAcSg==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-minify-selectors@8.0.1: resolution: {integrity: sha512-c31D46811kTkQDxV1KTTow79axX6gj/01AY5G7cGZg3s31KvAwP13jEFXGAzQbJ7NvOFV1pRqEia6nrAdHU7qg==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-charset@8.0.0: resolution: {integrity: sha512-s88FUNDSUD8m0wBYvTQQcubVts6zhXwBU8zCD4vkRKiecd0v8cOjHVIF9r/i+5xzS/WG3f98qq4XsOM0JqvfLA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-display-values@8.0.0: resolution: {integrity: sha512-gG2nBxD27fiw6Luinb1QYKdM/Co5GornRJgSD+JTwNH4PGKxImP0qyruDDav49aHUPLY3qrL3qN3LvybO7IzxQ==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-positions@8.0.0: resolution: {integrity: sha512-t/wGqpehS20Ke7kc4QAsWpH+AJjUdMK/V5qV2RhrXkj8hO/fT1t1MJ8NL7sedWYk7ZqC7eISEJQonW5j0tU1MQ==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-repeat-style@8.0.0: resolution: {integrity: sha512-3ebOmGdCYKrBYyGKc1xhj0unEnW7beZpVU7JohVeGl7mTxR+7T6egpaawTWAVsB0pEIhcsbJVOjPKCJSoRO6Zg==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-string@8.0.0: resolution: {integrity: sha512-TvWCGZ/e04Tv31uJvOUtbexkfgUnqmQ3M2P5DkAaVzvOj+BvTkG2QjpA5Y71SL1SPxJcj4M23fNh+RDVCmG8kA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-timing-functions@8.0.0: resolution: {integrity: sha512-uEfaXst5Xgqxv7geYUuz6vs9mn88K2NPY2RoIzM3BMmSjsdTSeppV9x2qIgrxsisdbSqF6IVhzI2occcte3hTA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-unicode@8.0.0: resolution: {integrity: sha512-+WYngZaChEeTHZmWhmKtnJ4gTzWdINEaFcgWBnu6WdVu8Ftim8OBTcw768DuCC/3Aax9bZ9WkwrLGHym2Lzf+A==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-url@8.0.0: resolution: {integrity: sha512-4Mz9hZHn/QIB+YtFqTXrDmE2193GYxGb3F8uMfLvMicaEXCCUlDIJ658gFFJbqEGl9FYzwPtRiuNgbwlO9kkBg==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-normalize-whitespace@8.0.0: resolution: {integrity: sha512-V1f8tYnwIP5tscOXQFTKK8Y5EJ+R2GMpFJ6FjzwoKoQnhbqQy3IeSrDjJJb8JjVos8ut6Osi80Zybpayv/XjIQ==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-ordered-values@8.0.0: resolution: {integrity: sha512-Dg9+itb6lmD0bxqhQyHCtXAwYRh0wUrx6Mp4/BNXgkLoJmdYMmWi+V+Pypw79Q6iQhxA8KFMHqLBITQJV2gKMA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-reduce-initial@8.0.0: resolution: {integrity: sha512-DChcE9d528AKrlpCTHjhsAiOsWCk4H9ApHPS1QqRT3praObWTiWyn6W1UddGpc46K9LQnHwUu4YwaPUukGtXVA==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-reduce-transforms@8.0.0: resolution: {integrity: sha512-cLZT0som7vvumQT9XQCnSKOSnRinNQZd1Hm+J723Ney13E8CIydDhw6JwzsjPtgnYThTqn9Q45906gz6wxaAsw==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} @@ -14872,19 +14876,19 @@ packages: resolution: {integrity: sha512-Q2fMSYEiNE1ioDc/3sxvI24NdgA/MJno2XLNpOxgv8aCcJbym8mZY10/lDY5+AWCIc3Aiqzy2Wcp9/zaIXBZgQ==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-unique-selectors@8.0.0: resolution: {integrity: sha512-iObuolUX+ITJfMU2QQFQdh31JgSjNLPNjVs6YGAqBHvOvAWXMMNget6donQl83aQaeS32i5XeKZURUW/WBxIUw==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + postcss@8.5.22: + resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -15266,8 +15270,8 @@ packages: peerDependencies: react: '>= 16.3' - react-router@7.17.0: - resolution: {integrity: sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==} + react-router@7.18.1: + resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -16310,7 +16314,7 @@ packages: resolution: {integrity: sha512-sWyjaJvBqHoVKYPbQ8JRvrGSPaYWtWrJsU+fGVtwKB1GE1rRPu3rC7T6UCuXLoL00Dwb+tsHe2T904r8Vnsx8w==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} peerDependencies: - postcss: ^8.5.10 + postcss: ^8.5.18 stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} @@ -16395,8 +16399,8 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tar@7.5.20: - resolution: {integrity: sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==} + tar@7.5.21: + resolution: {integrity: sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==} engines: {node: '>=18'} termi-link@1.1.0: @@ -17071,8 +17075,8 @@ packages: engines: {node: '>=8'} hasBin: true - valibot@1.2.0: - resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -20318,7 +20322,7 @@ snapshots: npmlog: 5.0.1 rimraf: 3.0.2 semver: 7.8.1 - tar: 7.5.20 + tar: 7.5.21 transitivePeerDependencies: - encoding - supports-color @@ -20331,7 +20335,7 @@ snapshots: node-fetch: 2.7.0(encoding@0.1.13) nopt: 8.1.0 semver: 7.8.1 - tar: 7.5.20 + tar: 7.5.21 transitivePeerDependencies: - encoding - supports-color @@ -20472,9 +20476,9 @@ snapshots: '@next/env@14.2.35': {} - '@next/env@15.5.18': {} + '@next/env@15.5.21': {} - '@next/env@16.2.6': {} + '@next/env@16.2.11': {} '@next/eslint-plugin-next@15.5.4': dependencies: @@ -20487,52 +20491,52 @@ snapshots: '@mdx-js/loader': 3.1.1(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.28.1)) '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.6) - '@next/swc-darwin-arm64@15.5.18': + '@next/swc-darwin-arm64@15.5.21': optional: true - '@next/swc-darwin-arm64@16.2.6': + '@next/swc-darwin-arm64@16.2.11': optional: true - '@next/swc-darwin-x64@15.5.18': + '@next/swc-darwin-x64@15.5.21': optional: true - '@next/swc-darwin-x64@16.2.6': + '@next/swc-darwin-x64@16.2.11': optional: true - '@next/swc-linux-arm64-gnu@15.5.18': + '@next/swc-linux-arm64-gnu@15.5.21': optional: true - '@next/swc-linux-arm64-gnu@16.2.6': + '@next/swc-linux-arm64-gnu@16.2.11': optional: true - '@next/swc-linux-arm64-musl@15.5.18': + '@next/swc-linux-arm64-musl@15.5.21': optional: true - '@next/swc-linux-arm64-musl@16.2.6': + '@next/swc-linux-arm64-musl@16.2.11': optional: true - '@next/swc-linux-x64-gnu@15.5.18': + '@next/swc-linux-x64-gnu@15.5.21': optional: true - '@next/swc-linux-x64-gnu@16.2.6': + '@next/swc-linux-x64-gnu@16.2.11': optional: true - '@next/swc-linux-x64-musl@15.5.18': + '@next/swc-linux-x64-musl@15.5.21': optional: true - '@next/swc-linux-x64-musl@16.2.6': + '@next/swc-linux-x64-musl@16.2.11': optional: true - '@next/swc-win32-arm64-msvc@15.5.18': + '@next/swc-win32-arm64-msvc@15.5.21': optional: true - '@next/swc-win32-arm64-msvc@16.2.6': + '@next/swc-win32-arm64-msvc@16.2.11': optional: true - '@next/swc-win32-x64-msvc@15.5.18': + '@next/swc-win32-x64-msvc@15.5.21': optional: true - '@next/swc-win32-x64-msvc@16.2.6': + '@next/swc-win32-x64-msvc@16.2.11': optional: true '@ngrok/ngrok-android-arm64@1.6.0': @@ -20846,9 +20850,9 @@ snapshots: '@rollup/plugin-replace': 6.0.3(rollup@4.60.3) '@vitejs/plugin-vue': 6.0.7(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.2)) '@vitejs/plugin-vue-jsx': 5.1.5(supports-color@8.1.1)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(vue@3.5.35(typescript@6.0.2)) - autoprefixer: 10.5.0(postcss@8.5.10) + autoprefixer: 10.5.0(postcss@8.5.22) consola: 3.4.2 - cssnano: 8.0.1(postcss@8.5.10) + cssnano: 8.0.1(postcss@8.5.22) defu: 6.1.7 escape-string-regexp: 5.0.0 exsolve: 1.0.8 @@ -20862,7 +20866,7 @@ snapshots: nypm: 0.6.6 pathe: 2.0.3 pkg-types: 2.3.1 - postcss: 8.5.10 + postcss: 8.5.22 seroval: 1.5.4 std-env: 4.1.0 ufo: 1.6.4 @@ -22361,7 +22365,7 @@ snapshots: dependencies: react: 19.2.6 - '@react-router/dev@7.13.2(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0)': + '@react-router/dev@7.13.2(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.0 @@ -22370,7 +22374,7 @@ snapshots: '@babel/preset-typescript': 7.27.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 - '@react-router/node': 7.13.2(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) + '@react-router/node': 7.13.2(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) '@remix-run/node-fetch-server': 0.13.0 arg: 5.0.2 babel-dead-code-elimination: 1.0.12(supports-color@8.1.1) @@ -22387,14 +22391,14 @@ snapshots: pkg-types: 2.3.0 prettier: 3.8.1 react-refresh: 0.14.2 - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) semver: 7.7.4 tinyglobby: 0.2.15 - valibot: 1.2.0(typescript@6.0.2) + valibot: 1.4.2(typescript@6.0.2) vite: 8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) optionalDependencies: - '@react-router/serve': 7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2) + '@react-router/serve': 7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: - '@types/node' @@ -22411,7 +22415,7 @@ snapshots: - tsx - yaml - '@react-router/dev@7.17.0(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0)': + '@react-router/dev@7.17.0(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.0 @@ -22420,7 +22424,7 @@ snapshots: '@babel/preset-typescript': 7.27.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.7 - '@react-router/node': 7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) + '@react-router/node': 7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) '@remix-run/node-fetch-server': 0.13.0 arg: 5.0.2 babel-dead-code-elimination: 1.0.12(supports-color@8.1.1) @@ -22437,14 +22441,14 @@ snapshots: pkg-types: 2.3.1 prettier: 3.8.1 react-refresh: 0.14.2 - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) semver: 7.8.1 tinyglobby: 0.2.16 - valibot: 1.2.0(typescript@6.0.2) + valibot: 1.4.2(typescript@6.0.2) vite: 7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) vite-node: 3.2.4(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0) optionalDependencies: - '@react-router/serve': 7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2) + '@react-router/serve': 7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: - '@types/node' @@ -22461,52 +22465,52 @@ snapshots: - tsx - yaml - '@react-router/express@7.17.0(express@4.22.1(supports-color@8.1.1))(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2)': + '@react-router/express@7.17.0(express@4.22.1(supports-color@8.1.1))(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2)': dependencies: - '@react-router/node': 7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) + '@react-router/node': 7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) express: 4.22.1(supports-color@8.1.1) - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) optionalDependencies: typescript: 6.0.2 - '@react-router/fs-routes@7.17.0(@react-router/dev@7.17.0(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2)': + '@react-router/fs-routes@7.17.0(@react-router/dev@7.17.0(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2)': dependencies: - '@react-router/dev': 7.17.0(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) + '@react-router/dev': 7.17.0(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) minimatch: 9.0.7 optionalDependencies: typescript: 6.0.2 - '@react-router/fs-routes@7.4.0(@react-router/dev@7.13.2(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2)': + '@react-router/fs-routes@7.4.0(@react-router/dev@7.13.2(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0))(typescript@6.0.2)': dependencies: - '@react-router/dev': 7.13.2(@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) + '@react-router/dev': 7.13.2(@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2))(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0) minimatch: 9.0.7 optionalDependencies: typescript: 6.0.2 - '@react-router/node@7.13.2(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2)': + '@react-router/node@7.13.2(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) optionalDependencies: typescript: 6.0.2 - '@react-router/node@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2)': + '@react-router/node@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) optionalDependencies: typescript: 6.0.2 - '@react-router/serve@7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2)': + '@react-router/serve@7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(supports-color@8.1.1)(typescript@6.0.2)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - '@react-router/express': 7.17.0(express@4.22.1(supports-color@8.1.1))(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) - '@react-router/node': 7.17.0(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) + '@react-router/express': 7.17.0(express@4.22.1(supports-color@8.1.1))(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) + '@react-router/node': 7.17.0(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@6.0.2) compression: 1.8.1(supports-color@8.1.1) express: 4.22.1(supports-color@8.1.1) get-port: 5.1.1 morgan: 1.11.0(supports-color@8.1.1) - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) source-map-support: 0.5.21 transitivePeerDependencies: - supports-color @@ -22846,7 +22850,7 @@ snapshots: dependencies: '@sentry/core': 10.59.0 - '@sentry/nextjs@10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': + '@sentry/nextjs@10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': dependencies: '@opentelemetry/api': 1.9.1 '@rollup/plugin-commonjs': 28.0.1(rollup@4.60.3) @@ -22859,7 +22863,7 @@ snapshots: '@sentry/react': 10.59.0(react@19.2.6) '@sentry/vercel-edge': 10.59.0 '@sentry/webpack-plugin': 5.3.0(encoding@0.1.13)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.28.1)) - next: 15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) rollup: 4.60.3 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -22872,7 +22876,7 @@ snapshots: - vite - webpack - '@sentry/nextjs@10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': + '@sentry/nextjs@10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))': dependencies: '@opentelemetry/api': 1.9.1 '@rollup/plugin-commonjs': 28.0.1(rollup@4.60.3) @@ -22885,7 +22889,7 @@ snapshots: '@sentry/react': 10.59.0(react@19.2.6) '@sentry/vercel-edge': 10.59.0 '@sentry/webpack-plugin': 5.3.0(encoding@0.1.13)(supports-color@8.1.1)(webpack@5.105.4(esbuild@0.28.1)) - next: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) rollup: 4.60.3 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -23766,7 +23770,7 @@ snapshots: '@alloc/quick-lru': 5.2.0 '@tailwindcss/node': 4.2.4 '@tailwindcss/oxide': 4.2.4 - postcss: 8.5.10 + postcss: 8.5.22 tailwindcss: 4.2.4 '@tailwindcss/typography@0.5.19(tailwindcss@4.2.4)': @@ -24997,7 +25001,7 @@ snapshots: '@vue/shared': 3.5.35 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.10 + postcss: 8.5.22 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.35': @@ -25317,7 +25321,7 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -25540,13 +25544,13 @@ snapshots: dependencies: gulp-header: 1.8.12 - autoprefixer@10.5.0(postcss@8.5.10): + autoprefixer@10.5.0(postcss@8.5.22): dependencies: browserslist: 4.28.2 caniuse-lite: 1.0.30001792 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -25723,6 +25727,10 @@ snapshots: dependencies: balanced-match: 4.0.4 + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -25825,7 +25833,7 @@ snapshots: minipass-pipeline: 1.2.4 p-map: 4.0.0 ssri: 10.0.6 - tar: 7.5.20 + tar: 7.5.21 unique-filename: 3.0.0 call-bind-apply-helpers@1.0.2: @@ -26449,48 +26457,48 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@8.0.1(postcss@8.5.10): + cssnano-preset-default@8.0.1(postcss@8.5.22): dependencies: browserslist: 4.28.2 - cssnano-utils: 6.0.0(postcss@8.5.10) - postcss: 8.5.10 - postcss-calc: 10.1.1(postcss@8.5.10) - postcss-colormin: 8.0.0(postcss@8.5.10) - postcss-convert-values: 8.0.0(postcss@8.5.10) - postcss-discard-comments: 8.0.0(postcss@8.5.10) - postcss-discard-duplicates: 8.0.0(postcss@8.5.10) - postcss-discard-empty: 8.0.0(postcss@8.5.10) - postcss-discard-overridden: 8.0.0(postcss@8.5.10) - postcss-merge-longhand: 8.0.0(postcss@8.5.10) - postcss-merge-rules: 8.0.0(postcss@8.5.10) - postcss-minify-font-values: 8.0.0(postcss@8.5.10) - postcss-minify-gradients: 8.0.0(postcss@8.5.10) - postcss-minify-params: 8.0.0(postcss@8.5.10) - postcss-minify-selectors: 8.0.1(postcss@8.5.10) - postcss-normalize-charset: 8.0.0(postcss@8.5.10) - postcss-normalize-display-values: 8.0.0(postcss@8.5.10) - postcss-normalize-positions: 8.0.0(postcss@8.5.10) - postcss-normalize-repeat-style: 8.0.0(postcss@8.5.10) - postcss-normalize-string: 8.0.0(postcss@8.5.10) - postcss-normalize-timing-functions: 8.0.0(postcss@8.5.10) - postcss-normalize-unicode: 8.0.0(postcss@8.5.10) - postcss-normalize-url: 8.0.0(postcss@8.5.10) - postcss-normalize-whitespace: 8.0.0(postcss@8.5.10) - postcss-ordered-values: 8.0.0(postcss@8.5.10) - postcss-reduce-initial: 8.0.0(postcss@8.5.10) - postcss-reduce-transforms: 8.0.0(postcss@8.5.10) - postcss-svgo: 8.0.0(postcss@8.5.10) - postcss-unique-selectors: 8.0.0(postcss@8.5.10) + cssnano-utils: 6.0.0(postcss@8.5.22) + postcss: 8.5.22 + postcss-calc: 10.1.1(postcss@8.5.22) + postcss-colormin: 8.0.0(postcss@8.5.22) + postcss-convert-values: 8.0.0(postcss@8.5.22) + postcss-discard-comments: 8.0.0(postcss@8.5.22) + postcss-discard-duplicates: 8.0.0(postcss@8.5.22) + postcss-discard-empty: 8.0.0(postcss@8.5.22) + postcss-discard-overridden: 8.0.0(postcss@8.5.22) + postcss-merge-longhand: 8.0.0(postcss@8.5.22) + postcss-merge-rules: 8.0.0(postcss@8.5.22) + postcss-minify-font-values: 8.0.0(postcss@8.5.22) + postcss-minify-gradients: 8.0.0(postcss@8.5.22) + postcss-minify-params: 8.0.0(postcss@8.5.22) + postcss-minify-selectors: 8.0.1(postcss@8.5.22) + postcss-normalize-charset: 8.0.0(postcss@8.5.22) + postcss-normalize-display-values: 8.0.0(postcss@8.5.22) + postcss-normalize-positions: 8.0.0(postcss@8.5.22) + postcss-normalize-repeat-style: 8.0.0(postcss@8.5.22) + postcss-normalize-string: 8.0.0(postcss@8.5.22) + postcss-normalize-timing-functions: 8.0.0(postcss@8.5.22) + postcss-normalize-unicode: 8.0.0(postcss@8.5.22) + postcss-normalize-url: 8.0.0(postcss@8.5.22) + postcss-normalize-whitespace: 8.0.0(postcss@8.5.22) + postcss-ordered-values: 8.0.0(postcss@8.5.22) + postcss-reduce-initial: 8.0.0(postcss@8.5.22) + postcss-reduce-transforms: 8.0.0(postcss@8.5.22) + postcss-svgo: 8.0.0(postcss@8.5.22) + postcss-unique-selectors: 8.0.0(postcss@8.5.22) - cssnano-utils@6.0.0(postcss@8.5.10): + cssnano-utils@6.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 - cssnano@8.0.1(postcss@8.5.10): + cssnano@8.0.1(postcss@8.5.22): dependencies: - cssnano-preset-default: 8.0.1(postcss@8.5.10) + cssnano-preset-default: 8.0.1(postcss@8.5.22) lilconfig: 3.1.3 - postcss: 8.5.10 + postcss: 8.5.22 csso@5.0.5: dependencies: @@ -27764,7 +27772,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.2: {} + fast-uri@3.1.4: {} fast-wrap-ansi@0.2.2: dependencies: @@ -27897,13 +27905,13 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flags@4.0.1(@opentelemetry/api@1.9.1)(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + flags@4.0.1(@opentelemetry/api@1.9.1)(next@16.2.11(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: '@edge-runtime/cookies': 5.0.2 jose: 5.9.6 optionalDependencies: '@opentelemetry/api': 1.9.1 - next: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -28730,7 +28738,7 @@ snapshots: immutable@3.8.3: {} - immutable@4.3.8: + immutable@4.3.9: optional: true import-fresh@3.3.0: @@ -30749,7 +30757,7 @@ snapshots: minimatch@10.2.3: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.8 minimatch@3.1.4: dependencies: @@ -30958,7 +30966,7 @@ snapshots: stacktrace-js: 2.0.2 stylis: 4.3.6 - nanoid@3.3.11: {} + nanoid@3.3.16: {} nanotar@0.3.0: {} @@ -30979,12 +30987,12 @@ snapshots: neo-async@2.6.2: {} - next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1): + next-contentlayer2@0.4.6(contentlayer2@0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.28.1)(markdown-wasm@1.2.0)(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(supports-color@8.1.1): dependencies: '@contentlayer2/core': 0.4.3(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1) '@contentlayer2/utils': 0.4.3 contentlayer2: 0.4.6(esbuild@0.28.1)(markdown-wasm@1.2.0)(supports-color@8.1.1) - next: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) transitivePeerDependencies: @@ -31013,14 +31021,14 @@ snapshots: dependencies: js-yaml-loader: 1.2.2 - next-router-mock@0.9.13(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6): + next-router-mock@0.9.13(next@16.2.11(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6): dependencies: - next: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) react: 19.2.6 - next-seo@6.5.0(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + next-seo@6.5.0(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - next: 15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -31031,24 +31039,24 @@ snapshots: next-tick@1.1.0: {} - next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4): + next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4): dependencies: - '@next/env': 15.5.18 + '@next/env': 15.5.21 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001792 - postcss: 8.5.10 + postcss: 8.5.22 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@8.1.1))(babel-plugin-macros@3.1.0)(react@19.2.6) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.18 - '@next/swc-darwin-x64': 15.5.18 - '@next/swc-linux-arm64-gnu': 15.5.18 - '@next/swc-linux-arm64-musl': 15.5.18 - '@next/swc-linux-x64-gnu': 15.5.18 - '@next/swc-linux-x64-musl': 15.5.18 - '@next/swc-win32-arm64-msvc': 15.5.18 - '@next/swc-win32-x64-msvc': 15.5.18 + '@next/swc-darwin-arm64': 15.5.21 + '@next/swc-darwin-x64': 15.5.21 + '@next/swc-linux-arm64-gnu': 15.5.21 + '@next/swc-linux-arm64-musl': 15.5.21 + '@next/swc-linux-x64-gnu': 15.5.21 + '@next/swc-linux-x64-musl': 15.5.21 + '@next/swc-win32-arm64-msvc': 15.5.21 + '@next/swc-win32-x64-msvc': 15.5.21 '@opentelemetry/api': 1.9.1 '@playwright/test': 1.59.1 sass: 1.77.4 @@ -31057,25 +31065,25 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4): + next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4): dependencies: - '@next/env': 16.2.6 + '@next/env': 16.2.11 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.29 caniuse-lite: 1.0.30001792 - postcss: 8.5.10 + postcss: 8.5.22 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@8.1.1))(babel-plugin-macros@3.1.0)(react@19.2.6) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.6 - '@next/swc-darwin-x64': 16.2.6 - '@next/swc-linux-arm64-gnu': 16.2.6 - '@next/swc-linux-arm64-musl': 16.2.6 - '@next/swc-linux-x64-gnu': 16.2.6 - '@next/swc-linux-x64-musl': 16.2.6 - '@next/swc-win32-arm64-msvc': 16.2.6 - '@next/swc-win32-x64-msvc': 16.2.6 + '@next/swc-darwin-arm64': 16.2.11 + '@next/swc-darwin-x64': 16.2.11 + '@next/swc-linux-arm64-gnu': 16.2.11 + '@next/swc-linux-arm64-musl': 16.2.11 + '@next/swc-linux-x64-gnu': 16.2.11 + '@next/swc-linux-x64-musl': 16.2.11 + '@next/swc-win32-arm64-msvc': 16.2.11 + '@next/swc-win32-x64-msvc': 16.2.11 '@opentelemetry/api': 1.9.1 '@playwright/test': 1.59.1 sass: 1.77.4 @@ -31228,7 +31236,7 @@ snapshots: nopt: 7.2.1 proc-log: 3.0.0 semver: 7.8.1 - tar: 7.5.20 + tar: 7.5.21 which: 4.0.0 transitivePeerDependencies: - supports-color @@ -31332,28 +31340,28 @@ snapshots: number-flow@0.3.7: {} - nuqs@1.19.1(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4)): + nuqs@1.19.1(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4)): dependencies: mitt: 3.0.1 - next: 15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) - nuqs@2.7.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6): + nuqs@2.7.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.6 optionalDependencies: '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - next: 16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next: 16.2.11(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - nuqs@2.8.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6): + nuqs@2.8.1(@tanstack/react-router@1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react@19.2.6): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.6 optionalDependencies: '@tanstack/react-router': 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - next: 15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) - react-router: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + next: 15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) nuxt@4.4.8(@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.7(supports-color@8.1.1)))(@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.7(supports-color@8.1.1)))(@electric-sql/pglite@0.4.5)(@parcel/watcher@2.5.6)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.35)(aws4fetch@1.0.20)(cac@6.7.14)(db0@0.3.4(@electric-sql/pglite@0.4.5))(encoding@0.1.13)(eslint@9.37.0(jiti@2.7.0)(supports-color@8.1.1))(ioredis@5.10.1(supports-color@8.1.1))(lightningcss@1.32.0)(magicast@0.5.2)(rolldown@1.0.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.3)(rollup@4.60.3))(rollup@4.60.3)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(yaml@2.9.0): dependencies: @@ -32065,144 +32073,144 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-calc@10.1.1(postcss@8.5.10): + postcss-calc@10.1.1(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-colormin@8.0.0(postcss@8.5.10): + postcss-colormin@8.0.0(postcss@8.5.22): dependencies: '@colordx/core': 5.4.3 browserslist: 4.28.2 caniuse-api: 3.0.0 - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-convert-values@8.0.0(postcss@8.5.10): + postcss-convert-values@8.0.0(postcss@8.5.22): dependencies: browserslist: 4.28.2 - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-discard-comments@8.0.0(postcss@8.5.10): + postcss-discard-comments@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-selector-parser: 7.1.1 - postcss-discard-duplicates@8.0.0(postcss@8.5.10): + postcss-discard-duplicates@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 - postcss-discard-empty@8.0.0(postcss@8.5.10): + postcss-discard-empty@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 - postcss-discard-overridden@8.0.0(postcss@8.5.10): + postcss-discard-overridden@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 - postcss-merge-longhand@8.0.0(postcss@8.5.10): + postcss-merge-longhand@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - stylehacks: 8.0.0(postcss@8.5.10) + stylehacks: 8.0.0(postcss@8.5.22) - postcss-merge-rules@8.0.0(postcss@8.5.10): + postcss-merge-rules@8.0.0(postcss@8.5.22): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 - cssnano-utils: 6.0.0(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 6.0.0(postcss@8.5.22) + postcss: 8.5.22 postcss-selector-parser: 7.1.1 - postcss-minify-font-values@8.0.0(postcss@8.5.10): + postcss-minify-font-values@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-minify-gradients@8.0.0(postcss@8.5.10): + postcss-minify-gradients@8.0.0(postcss@8.5.22): dependencies: '@colordx/core': 5.4.3 - cssnano-utils: 6.0.0(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 6.0.0(postcss@8.5.22) + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-minify-params@8.0.0(postcss@8.5.10): + postcss-minify-params@8.0.0(postcss@8.5.22): dependencies: browserslist: 4.28.2 - cssnano-utils: 6.0.0(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 6.0.0(postcss@8.5.22) + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-minify-selectors@8.0.1(postcss@8.5.10): + postcss-minify-selectors@8.0.1(postcss@8.5.22): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 cssesc: 3.0.0 - postcss: 8.5.10 + postcss: 8.5.22 postcss-selector-parser: 7.1.1 - postcss-normalize-charset@8.0.0(postcss@8.5.10): + postcss-normalize-charset@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 - postcss-normalize-display-values@8.0.0(postcss@8.5.10): + postcss-normalize-display-values@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-normalize-positions@8.0.0(postcss@8.5.10): + postcss-normalize-positions@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@8.0.0(postcss@8.5.10): + postcss-normalize-repeat-style@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-normalize-string@8.0.0(postcss@8.5.10): + postcss-normalize-string@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@8.0.0(postcss@8.5.10): + postcss-normalize-timing-functions@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@8.0.0(postcss@8.5.10): + postcss-normalize-unicode@8.0.0(postcss@8.5.22): dependencies: browserslist: 4.28.2 - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-normalize-url@8.0.0(postcss@8.5.10): + postcss-normalize-url@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@8.0.0(postcss@8.5.10): + postcss-normalize-whitespace@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-ordered-values@8.0.0(postcss@8.5.10): + postcss-ordered-values@8.0.0(postcss@8.5.22): dependencies: - cssnano-utils: 6.0.0(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 6.0.0(postcss@8.5.22) + postcss: 8.5.22 postcss-value-parser: 4.2.0 - postcss-reduce-initial@8.0.0(postcss@8.5.10): + postcss-reduce-initial@8.0.0(postcss@8.5.22): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 - postcss: 8.5.10 + postcss: 8.5.22 - postcss-reduce-transforms@8.0.0(postcss@8.5.10): + postcss-reduce-transforms@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 postcss-selector-parser@6.0.10: @@ -32215,22 +32223,22 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@8.0.0(postcss@8.5.10): + postcss-svgo@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-value-parser: 4.2.0 svgo: 4.0.2 - postcss-unique-selectors@8.0.0(postcss@8.5.10): + postcss-unique-selectors@8.0.0(postcss@8.5.22): dependencies: - postcss: 8.5.10 + postcss: 8.5.22 postcss-selector-parser: 7.1.1 postcss-value-parser@4.2.0: {} - postcss@8.5.10: + postcss@8.5.22: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -32667,7 +32675,7 @@ snapshots: transitivePeerDependencies: - react-dom - react-router@7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: cookie: 1.0.2 react: 19.2.6 @@ -33330,7 +33338,7 @@ snapshots: sass@1.77.4: dependencies: chokidar: 3.6.0 - immutable: 4.3.8 + immutable: 4.3.9 source-map-js: 1.2.1 optional: true @@ -33537,7 +33545,7 @@ snapshots: msw: 2.11.3(@types/node@22.13.14)(typescript@6.0.2) node-fetch: 3.3.2 ora: 8.2.0 - postcss: 8.5.10 + postcss: 8.5.22 prompts: 2.4.2 recast: 0.23.11 stringify-object: 5.0.0 @@ -34065,10 +34073,10 @@ snapshots: '@babel/core': 7.29.7(supports-color@8.1.1) babel-plugin-macros: 3.1.0 - stylehacks@8.0.0(postcss@8.5.10): + stylehacks@8.0.0(postcss@8.5.22): dependencies: browserslist: 4.28.2 - postcss: 8.5.10 + postcss: 8.5.22 postcss-selector-parser: 7.1.1 stylis@4.3.6: {} @@ -34086,7 +34094,7 @@ snapshots: bin-links: 6.0.0 https-proxy-agent: 7.0.6(supports-color@8.1.1) node-fetch: 3.3.2 - tar: 7.5.20 + tar: 7.5.21 transitivePeerDependencies: - supports-color @@ -34159,7 +34167,7 @@ snapshots: fast-fifo: 1.3.2 streamx: 2.22.0 - tar@7.5.20: + tar@7.5.21: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -34531,12 +34539,12 @@ snapshots: unicorn-magic@0.4.0: {} - unicornstudio-react@2.0.1-1(next@15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + unicornstudio-react@2.0.1-1(next@15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) optionalDependencies: - next: 15.5.18(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) + next: 15.5.21(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4) unified@10.1.2: dependencies: @@ -34837,7 +34845,7 @@ snapshots: kleur: 4.1.5 sade: 1.8.1 - valibot@1.2.0(typescript@6.0.2): + valibot@1.4.2(typescript@6.0.2): optionalDependencies: typescript: 6.0.2 @@ -35054,7 +35062,7 @@ snapshots: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.10 + postcss: 8.5.22 rollup: 4.60.3 tinyglobby: 0.2.17 optionalDependencies: @@ -35071,7 +35079,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.10 + postcss: 8.5.22 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 31828a1e734..0c760aabf7f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -37,9 +37,9 @@ catalog: lodash: ^4.18.1 lodash-es: ^4.18.1 monaco-editor: 0.52.2 - next: 16.2.6 + next: ^16.2.11 next-themes: ^0.4.6 - postcss: ^8.5.10 + postcss: ^8.5.18 radix-ui: ^1.4.3 react: ^19.2.6 react-dom: ^19.2.6 @@ -94,12 +94,12 @@ minimumReleaseAgeExclude: overrides: '@ardatan/relay-compiler>immutable': ^3.8.3 'monaco-editor': 'catalog:' - '@mapbox/node-pre-gyp>tar': ^7.5.19 + '@mapbox/node-pre-gyp>tar': ^7.5.21 '@sentry/webpack-plugin>uuid': ^11.1.1 '@usercentrics/cmp-browser-sdk>uuid': ^11.1.1 braintrust>esbuild: ^0.28.1 braintrust>uuid: ^11.1.1 - cacache>tar: ^7.5.19 + cacache>tar: ^7.5.21 dompurify: ^3.3.2 express-rate-limit>ip-address: ^10.1.1 # Pin h3 v1 to a single version so the Nuxt registry example (vue-blocks) @@ -109,12 +109,12 @@ overrides: lodash: 'catalog:' lodash-es: 'catalog:' mdx-bundler>uuid: ^11.1.1 - node-gyp>tar: ^7.5.19 + node-gyp>tar: ^7.5.21 nodemailer: ^7.0.11 postcss: 'catalog:' qs: ^6.15.2 refractor>prismjs: ^1.30.0 - supabase>tar: ^7.5.19 + supabase>tar: ^7.5.21 tmp: ^0.2.7 vite>esbuild: ^0.28.1 webpack: ^5.104.1 From c16c7e94ccd906015a11d6c0d7efcdb87b82f4b8 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:38:11 -0400 Subject: [PATCH 124/141] =?UTF-8?q?feat(studio):=20SQL=20editor=20logs=20s?= =?UTF-8?q?ource=20=E2=80=94=20toolbar=20UI=20+=20creation=20flow=20(#4845?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What PR 6 of the SQL-editor "query source (Database vs Logs)" stack (builds on the merged PR 5, #48414). Adds the user-facing toolbar surface for the logs query source and consolidates the SQL-editor toolbar into a single **source menu**. Everything stays behind `sqlEditorLogsSource` + `otelLegacyLogs` (dual-flag gated); with the flags off the toolbar is unchanged. ## Changes - **Consolidated source menu** (`QuerySourceMenu`) — one `Database ▾` / `Logs ▾` dropdown that both labels the snippet's source and hosts the source-specific controls as flyout submenus: - Database: database selector (`Primary` / read replicas), `Run as` (role impersonation), and `Row limit`. - Logs: `Time range` — the same relative presets as the Logs Explorer plus a `Custom range…` calendar dialog. - **Source is immutable** — the Database/Logs rows aren't a toggle. An existing (materialized) snippet opens a *fresh* tab of the target source (never reinterpreting a query against the wrong backend); a blank new tab re-flavors in place. Extracted as the pure, unit-tested `resolveSourceSwitch`. - **New-snippet-with-source** threaded through `/sql/new?source=`, the nav "Create a new logs query" entry, and the duplicate flow. Logs snippets hide the (db-dialect) Export action. - **Run-affordance guard** — the Run button is disabled + annotated for a logs snippet on a non-ClickHouse org (sits above PR 5's execution short-circuit). - **Retention entitlement gating** — both preset and custom logs ranges past `log.retention_days` surface the upgrade prompt instead of applying silently. Prettify is disabled for logs (sql-formatter mangles ClickHouse). ## Tests - `querySource.test.ts` — `logDateRangesEqual` (structural relative/absolute matching, incl. the "Last hour" vs "Last 1 hour" label case). - `QuerySourceMenu.utils.test.ts` — `resolveSourceSwitch` push-vs-replace / no-op behavior. `pnpm --filter studio typecheck` · `lint:ratchet` · Prettier · SQL editor suite (307 tests) all green. ## For reviewers To test manually, enable the `sqlEditorLogsSource` feature flag for yourself on local/staging. There is no nav for Log SQL snippets currently (that is by design, this PR is big enough as-is), so to check an existing logs snippet, you can create one using the existing Logs Explorer, copy its UUID, and force navigate to that snippet in the SQL editor via URL. ## Summary by CodeRabbit * **New Features** * Added support for creating and switching between database and logs queries. * Added log time-range presets and custom date-range selection. * Added database, run-as role, and row-limit controls. * Added read-replica selection, including options to create a new replica when available. * **Improvements** * Added clearer explanations when query execution is unavailable. * Disabled SQL formatting and query export where unsupported for logs queries. --------- Co-authored-by: Joshen Lim --- .../interfaces/SQLEditor/SQLEditorLayout.tsx | 1 + .../QuerySourceMenu/DatabaseSubMenu.tsx | 95 ++++++++++ .../QuerySourceMenu/LogsCustomRangeDialog.tsx | 58 ++++++ .../QuerySourceMenu/QuerySourceMenu.tsx | 167 ++++++++++++++++++ .../QuerySourceMenu.utils.test.ts | 60 +++++++ .../QuerySourceMenu/QuerySourceMenu.utils.ts | 43 +++++ .../QuerySourceMenu/RowLimitSubMenu.tsx | 38 ++++ .../QuerySourceMenu/RunAsSubMenu.tsx | 31 ++++ .../QuerySourceMenu/TimeRangeSubMenu.tsx | 88 +++++++++ .../SQLEditor/UtilityPanel/RunButton.tsx | 17 +- .../SQLEditor/UtilityPanel/UtilityActions.tsx | 106 +++++++---- .../interfaces/SQLEditor/querySource.test.ts | 55 ++++++ .../interfaces/SQLEditor/querySource.ts | 26 +++ .../interfaces/SQLEditor/usePrettifyQuery.ts | 7 +- .../interfaces/SQLEditor/useRunSource.ts | 23 ++- .../interfaces/SQLEditor/useSnippetEditor.ts | 6 +- .../layouts/SQLEditorLayout/SQLEditorMenu.tsx | 17 +- .../SQLEditorNavV2/SQLEditorTreeViewItem.tsx | 24 +-- 18 files changed, 800 insertions(+), 62 deletions(-) create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/DatabaseSubMenu.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/LogsCustomRangeDialog.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.test.ts create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.ts create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RowLimitSubMenu.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RunAsSubMenu.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/TimeRangeSubMenu.tsx diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx index d149bdeb95e..f229f200768 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorLayout.tsx @@ -95,6 +95,7 @@ const SQLEditorToolbar = () => { return ( { + const { ref: projectRef } = useParams() + const sessionSnap = useSqlEditorSessionSnapshot() + const dbSelector = useDatabaseSelectorStateSnapshot() + const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas']) + + const [lastSelectedDb, setLastSelectedDb] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.SQL_EDITOR_LAST_SELECTED_DB(projectRef ?? ''), + '' + ) + + const { data } = useReadReplicasQuery({ projectRef }) + const databases = (data ?? []) + .slice() + .sort((a, b) => (a.inserted_at > b.inserted_at ? 1 : 0)) + .sort((database) => (database.identifier === projectRef ? -1 : 0)) + + const selectedDatabaseId = + lastSelectedDb.length > 0 ? lastSelectedDb : (dbSelector.selectedDatabaseId ?? projectRef) + const selectedDatabase = databases.find((db) => db.identifier === selectedDatabaseId) + + const newReplicaURL = `/project/${projectRef}/database/replication?destinationType=Read+Replica` + + const handleSelect = (databaseId: string) => { + dbSelector.setSelectedDatabaseId(databaseId) + setLastSelectedDb(databaseId) + sessionSnap.resetResult(id) + } + + return ( + + +
    + Database + + {selectedDatabase + ? databaseLabel(selectedDatabase.identifier, selectedDatabase.region, projectRef) + : 'Primary database'} + +
    +
    + + {databases.map((database) => { + const isUnhealthy = database.status !== 'ACTIVE_HEALTHY' + return ( + handleSelect(database.identifier)} + > + {databaseLabel(database.identifier, database.region, projectRef)} + {database.identifier === selectedDatabaseId && } + + ) + })} + {infrastructureReadReplicas && ( + <> + + + + + Create a new read replica + + + + )} + +
    + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/LogsCustomRangeDialog.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/LogsCustomRangeDialog.tsx new file mode 100644 index 00000000000..76ba203f1df --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/LogsCustomRangeDialog.tsx @@ -0,0 +1,58 @@ +import { useState } from 'react' +import { type DateRange } from 'react-day-picker' +import { + Button, + Calendar, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from 'ui' + +export const LogsCustomRangeDialog = ({ + open, + onOpenChange, + onApply, +}: { + open: boolean + onOpenChange: (open: boolean) => void + onApply: (range: { from: Date; to: Date }) => void +}) => { + const [range, setRange] = useState() + + const canApply = range?.from !== undefined && range?.to !== undefined + + const handleApply = () => { + if (range?.from === undefined || range?.to === undefined) return + onApply({ from: range.from, to: range.to }) + onOpenChange(false) + } + + return ( + + + + Custom time range + +
    +
    + + + + +
    +
    + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.tsx new file mode 100644 index 00000000000..027ae76bd2c --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.tsx @@ -0,0 +1,167 @@ +import { useParams } from 'common' +import dayjs from 'dayjs' +import { Check, ChevronDown, Database, ScrollText } from 'lucide-react' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from 'ui' + +import { + datePickerValueToLogDateRange, + type QuerySource, + type SqlSnippetSource, +} from '../../querySource' +import { DatabaseSubMenu } from './DatabaseSubMenu' +import { LogsCustomRangeDialog } from './LogsCustomRangeDialog' +import { resolveSourceSwitch } from './QuerySourceMenu.utils' +import { RowLimitSubMenu } from './RowLimitSubMenu' +import { RunAsSubMenu } from './RunAsSubMenu' +import { TimeRangeSubMenu } from './TimeRangeSubMenu' +import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils' +import UpgradePrompt from '@/components/interfaces/Settings/Logs/UpgradePrompt' +import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' +import { IS_PLATFORM } from '@/lib/constants' +import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state' +import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state' + +const SOURCE_LABEL: Record = { + database: 'Database', + logs: 'Logs', +} + +const SourceIcon = ({ source, ...props }: { source: SqlSnippetSource; className?: string }) => + source === 'logs' ? : + +type QuerySourceMenuProps = { + id: string + runSource: QuerySource + /** Whether creating a logs snippet is available (feature-flagged). */ + canCreateLogsSnippet: boolean +} + +/** + * The consolidated SQL-editor source control: a single toolbar dropdown that + * both labels the snippet's source (Database / Logs) and hosts every + * source-specific control as a flyout submenu — database selector + role + * impersonation + row limit for a database snippet, time range for a logs + * snippet. The logs "Custom range…" calendar and the retention upgrade prompt are + * rendered as siblings of the dropdown (not inside it) so they survive the menu + * closing. + * + * A snippet's source is immutable, so the source rows aren't a plain toggle. + * Once a snippet exists — a saved query, or a new one the user has already typed + * into — switching backends opens a *fresh* tab with a new snippet of that + * source, leaving the existing query untouched (there's no sense running a query + * against the wrong backend). Only a brand-new, not-yet-materialized blank tab + * re-flavors in place, so we don't spawn a redundant snippet before the user has + * written anything. + */ +export const QuerySourceMenu = ({ id, runSource, canCreateLogsSnippet }: QuerySourceMenuProps) => { + const { ref } = useParams() + const router = useRouter() + const snapV2 = useSqlEditorV2StateSnapshot() + const sessionSnap = useSqlEditorSessionSnapshot() + + const [isCustomRangeOpen, setIsCustomRangeOpen] = useState(false) + const [showUpgradePrompt, setShowUpgradePrompt] = useState(false) + + const { getEntitlementNumericValue } = useCheckEntitlements('log.retention_days') + const entitledToLogDays = getEntitlementNumericValue() + + const currentSource = runSource.type + const isLogs = currentSource === 'logs' + // A snippet materializes in the store on its first keystroke; until then a + // `/sql/new` tab is a blank scaffold with nothing to preserve. + const isBlankNewTab = snapV2.snippets[id] === undefined + + const switchSource = (target: SqlSnippetSource) => { + const next = resolveSourceSwitch({ ref, target, currentSource, isBlankNewTab }) + if (next === null) return + router[next.method](next.url) + } + + const applyCustomRange = ({ from, to }: { from: Date; to: Date }) => { + const fromIso = dayjs(from).startOf('day').toISOString() + if (maybeShowUpgradePromptIfNotEntitled(fromIso, entitledToLogDays)) { + setShowUpgradePrompt(true) + return + } + sessionSnap.setLogRange( + id, + datePickerValueToLogDateRange({ + from: fromIso, + to: dayjs(to).endOf('day').toISOString(), + isHelper: false, + }) + ) + } + + return ( + <> + + + + + + switchSource('database')}> + + + Database + + {!isLogs && } + + {(canCreateLogsSnippet || isLogs) && ( + switchSource('logs')}> + + + Logs + + {isLogs && } + + )} + + + + {runSource.type === 'logs' ? ( + setIsCustomRangeOpen(true)} + onShowUpgrade={() => setShowUpgradePrompt(true)} + /> + ) : ( + <> + {IS_PLATFORM && } + + + + )} + + + + {isLogs && ( + <> + + + + )} + + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.test.ts b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.test.ts new file mode 100644 index 00000000000..d005b5b44b4 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' + +import { resolveSourceSwitch } from './QuerySourceMenu.utils' + +describe('resolveSourceSwitch', () => { + it('opens a new tab (push) when switching source on an existing snippet', () => { + expect( + resolveSourceSwitch({ + ref: 'abc', + target: 'logs', + currentSource: 'database', + isBlankNewTab: false, + }) + ).toEqual({ method: 'push', url: '/project/abc/sql/new?skip=true&source=logs' }) + }) + + it('re-flavors in place (replace) on a brand-new blank tab', () => { + expect( + resolveSourceSwitch({ + ref: 'abc', + target: 'logs', + currentSource: 'database', + isBlankNewTab: true, + }) + ).toEqual({ method: 'replace', url: '/project/abc/sql/new?skip=true&source=logs' }) + }) + + it('omits the source param when switching to database (the default source)', () => { + expect( + resolveSourceSwitch({ + ref: 'abc', + target: 'database', + currentSource: 'logs', + isBlankNewTab: false, + }) + ).toEqual({ method: 'push', url: '/project/abc/sql/new?skip=true' }) + }) + + it('is a no-op when the snippet is already on the target source', () => { + expect( + resolveSourceSwitch({ + ref: 'abc', + target: 'database', + currentSource: 'database', + isBlankNewTab: false, + }) + ).toBeNull() + }) + + it('is a no-op without a project ref', () => { + expect( + resolveSourceSwitch({ + ref: undefined, + target: 'logs', + currentSource: 'database', + isBlankNewTab: true, + }) + ).toBeNull() + }) +}) diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.ts b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.ts new file mode 100644 index 00000000000..0aba7a73b97 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/QuerySourceMenu.utils.ts @@ -0,0 +1,43 @@ +import { type SqlSnippetSource } from '../../querySource' + +export type SourceSwitchNavigation = { + /** + * `push` opens a new tab (leaving the current snippet intact); `replace` + * re-flavors the current blank tab in place without a history entry. + */ + method: 'push' | 'replace' + url: string +} + +/** + * Decide how switching a snippet's query source should navigate. A snippet's + * source is immutable, so switching never edits the current snippet: + * + * - An existing (materialized) snippet — a saved query, or a new one already + * typed into — opens a *fresh* `/sql/new` tab of the target source (`push`), so + * its query is never reinterpreted against the wrong backend. + * - A brand-new, not-yet-materialized blank tab re-flavors in place (`replace`), + * so we don't spawn a redundant snippet before anything has been written. + * + * Returns `null` when there's nothing to do (no project ref, or the snippet is + * already on the target source). + */ +export function resolveSourceSwitch({ + ref, + target, + currentSource, + isBlankNewTab, +}: { + ref: string | undefined + target: SqlSnippetSource + currentSource: SqlSnippetSource + isBlankNewTab: boolean +}): SourceSwitchNavigation | null { + if (!ref || target === currentSource) return null + const suffix = target === 'logs' ? '&source=logs' : '' + return { + method: isBlankNewTab ? 'replace' : 'push', + // skip=true bypasses the "load last visited snippet" redirect on /sql/new. + url: `/project/${ref}/sql/new?skip=true${suffix}`, + } +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RowLimitSubMenu.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RowLimitSubMenu.tsx new file mode 100644 index 00000000000..9015dbada13 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RowLimitSubMenu.tsx @@ -0,0 +1,38 @@ +import { + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, +} from 'ui' + +import { ROWS_PER_PAGE_OPTIONS } from '../../SQLEditor.constants' +import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state' + +export const RowLimitSubMenu = () => { + const sessionSnap = useSqlEditorSessionSnapshot() + const currentLabel = ROWS_PER_PAGE_OPTIONS.find((opt) => opt.value === sessionSnap.limit)?.label + + return ( + + +
    + Row limit + {currentLabel} +
    +
    + + sessionSnap.setLimit(Number(val))} + > + {ROWS_PER_PAGE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + +
    + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RunAsSubMenu.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RunAsSubMenu.tsx new file mode 100644 index 00000000000..a44a95d2d60 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/RunAsSubMenu.tsx @@ -0,0 +1,31 @@ +import { DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from 'ui' + +import { RoleImpersonationSelector } from '@/components/interfaces/RoleImpersonationSelector' +import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' + +const SERVICE_ROLE_LABEL = 'postgres' + +export const RunAsSubMenu = () => { + const state = useRoleImpersonationStateSnapshot() + const currentRole = state.role?.role ?? SERVICE_ROLE_LABEL + + return ( + + +
    + Run as + {currentRole} +
    +
    + {/* Stops propagation so the authenticated-user search input isn't swallowed by the + dropdown's typeahead. */} + e.stopPropagation()}> + + +
    + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/TimeRangeSubMenu.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/TimeRangeSubMenu.tsx new file mode 100644 index 00000000000..39517f8c7b6 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/QuerySourceMenu/TimeRangeSubMenu.tsx @@ -0,0 +1,88 @@ +import { Check, Lock } from 'lucide-react' +import { + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, +} from 'ui' + +import { + datePickerValueToLogDateRange, + logDateRangesEqual, + type LogDateRange, +} from '../../querySource' +import { EXPLORER_DATEPICKER_HELPERS } from '@/components/interfaces/Settings/Logs/Logs.constants' +import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils' +import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' +import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state' + +export const TimeRangeSubMenu = ({ + id, + range, + onOpenCustomRange, + onShowUpgrade, +}: { + id: string + range: LogDateRange + onOpenCustomRange: () => void + onShowUpgrade: () => void +}) => { + const sessionSnap = useSqlEditorSessionSnapshot() + const { getEntitlementNumericValue } = useCheckEntitlements('log.retention_days') + const entitledToLogDays = getEntitlementNumericValue() + + const isCustomRange = range.kind === 'absolute' + const presets = EXPLORER_DATEPICKER_HELPERS.map((helper) => ({ + helper, + range: datePickerValueToLogDateRange({ + from: helper.calcFrom(), + to: helper.calcTo(), + isHelper: true, + text: helper.text, + }), + })) + const selectedPreset = presets.find((preset) => logDateRangesEqual(range, preset.range)) + + return ( + + +
    + Time range + + {isCustomRange ? 'Custom range' : (selectedPreset?.helper.text ?? 'Custom range')} + +
    +
    + + {presets.map(({ helper, range: presetRange }) => { + const isSelected = !isCustomRange && logDateRangesEqual(range, presetRange) + const isLocked = maybeShowUpgradePromptIfNotEntitled(helper.calcFrom(), entitledToLogDays) + + return ( + { + if (isLocked) return onShowUpgrade() + sessionSnap.setLogRange(id, presetRange) + }} + > + {helper.text} + {isLocked ? ( + + ) : ( + isSelected && + )} + + ) + })} + + + Custom range… + {isCustomRange && } + + +
    + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx index 861481f457d..6fdaae2365e 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx @@ -1,5 +1,7 @@ import { Loader2 } from 'lucide-react' -import { Button, KeyboardShortcut } from 'ui' +import { KeyboardShortcut } from 'ui' + +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' interface SqlRunButtonProps { isDisabled?: boolean @@ -7,6 +9,13 @@ interface SqlRunButtonProps { hasSelection?: boolean className?: string onClick: () => void + /** + * Explanation shown in a tooltip while the button is disabled — e.g. why a + * logs query can't run on this project. A disabled ` + ) } diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx index 4a6b55cf89b..a5c69be3d40 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx @@ -1,5 +1,5 @@ import { Hotkey } from '@tanstack/react-hotkeys' -import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common' import { AlignLeft, Check, ChevronDown, Heart, Keyboard, MoreVertical } from 'lucide-react' import { toast } from 'sonner' import { @@ -18,8 +18,10 @@ import { TooltipTrigger, } from 'ui' +import { type QuerySource } from '../querySource' import { ROWS_PER_PAGE_OPTIONS } from '../SQLEditor.constants' import { AutosaveStatus } from './AutosaveStatus' +import { QuerySourceMenu } from './QuerySourceMenu/QuerySourceMenu' import { SqlRunButton } from './RunButton' import { SqlSaveButton } from './SaveButton' import SavingIndicator from './SavingIndicator' @@ -35,6 +37,7 @@ import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state export type UtilityActionsProps = { id: string + runSource: QuerySource isExecuting?: boolean isDisabled?: boolean hasSelection?: boolean @@ -45,6 +48,7 @@ export type UtilityActionsProps = { export const UtilityActions = ({ id, + runSource, isExecuting = false, isDisabled = false, hasSelection = false, @@ -57,6 +61,14 @@ export const UtilityActions = ({ const sessionSnap = useSqlEditorSessionSnapshot() const isManualSaveEnabled = useIsSqlEditorManualSaveEnabled() + const isLogsSourceEnabled = useFlag('sqlEditorLogsSource') + const isOtelLogsEnabled = useFlag('otelLegacyLogs') + + const isLogs = runSource.type === 'logs' + const canCreateLogsSnippet = isLogsSourceEnabled && isOtelLogsEnabled + const canShowSourceIndicator = isLogs || canCreateLogsSnippet + const isLogsRunBlocked = isLogs && !isOtelLogsEnabled + const [isAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true) const [intellisenseEnabled, setIntellisenseEnabled] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE, @@ -141,7 +153,7 @@ export const UtilityActions = ({ )} - + Prettify SQL @@ -208,6 +220,7 @@ export const UtilityActions = ({
    -
    - {IS_PLATFORM && ( - - )} - -
    + ) : ( + <> +
    + {IS_PLATFORM && ( + + )} + +
    - - - - - - sessionSnap.setLimit(Number(val))} - > - {ROWS_PER_PAGE_OPTIONS.map((option) => ( - - {option.label} - - ))} - - - + + + + + + sessionSnap.setLimit(Number(val))} + > + {ROWS_PER_PAGE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + + )}
    {isManualSaveEnabled && } diff --git a/apps/studio/components/interfaces/SQLEditor/querySource.test.ts b/apps/studio/components/interfaces/SQLEditor/querySource.test.ts index 9c63333f64e..5b79d3b7768 100644 --- a/apps/studio/components/interfaces/SQLEditor/querySource.test.ts +++ b/apps/studio/components/interfaces/SQLEditor/querySource.test.ts @@ -6,6 +6,7 @@ import { DEFAULT_LOG_DATE_RANGE, getSnippetSource, isoDateTimeString, + logDateRangesEqual, logDateRangeToDatePickerValue, resolveLogRunRange, type LogDateRange, @@ -228,3 +229,57 @@ describe('querySource.ts:resolveLogRunRange', () => { }) }) }) + +describe('querySource.ts:logDateRangesEqual', () => { + it('matches relative ranges on amount + unit regardless of label formatting', () => { + const lastHourPreset = EXPLORER_DATEPICKER_HELPERS.find((h) => h.text === 'Last hour')! + const presetRange = datePickerValueToLogDateRange({ + from: lastHourPreset.calcFrom(), + to: lastHourPreset.calcTo(), + isHelper: true, + text: lastHourPreset.text, + }) + + expect( + logDateRangesEqual(presetRange, { kind: 'relative', last: { amount: 1, unit: 'hour' } }) + ).toBe(true) + }) + + it('does not match relative ranges with a different amount or unit', () => { + expect( + logDateRangesEqual( + { kind: 'relative', last: { amount: 1, unit: 'hour' } }, + { kind: 'relative', last: { amount: 3, unit: 'hour' } } + ) + ).toBe(false) + expect( + logDateRangesEqual( + { kind: 'relative', last: { amount: 1, unit: 'hour' } }, + { kind: 'relative', last: { amount: 1, unit: 'day' } } + ) + ).toBe(false) + }) + + it('matches absolute ranges on their ISO endpoints', () => { + const from = isoDateTimeString('2025-01-01T00:00:00.000Z')! + const to = isoDateTimeString('2025-01-02T00:00:00.000Z')! + expect(logDateRangesEqual({ kind: 'absolute', from, to }, { kind: 'absolute', from, to })).toBe( + true + ) + }) + + it('never matches a relative range against an absolute one', () => { + const from = isoDateTimeString('2025-01-01T00:00:00.000Z')! + const to = isoDateTimeString('2025-01-02T00:00:00.000Z')! + expect( + logDateRangesEqual( + { kind: 'relative', last: { amount: 1, unit: 'hour' } }, + { + kind: 'absolute', + from, + to, + } + ) + ).toBe(false) + }) +}) diff --git a/apps/studio/components/interfaces/SQLEditor/querySource.ts b/apps/studio/components/interfaces/SQLEditor/querySource.ts index e4d2b305d56..1ba1c4fd1ea 100644 --- a/apps/studio/components/interfaces/SQLEditor/querySource.ts +++ b/apps/studio/components/interfaces/SQLEditor/querySource.ts @@ -25,6 +25,16 @@ export function getSnippetSource(snippet: Pick): SqlSnippetSour return snippet.type === 'log_sql' ? 'logs' : 'database' } +/** + * Parse a raw `source` value (e.g. the `?source=` query param a creation entry + * threads through `/sql/new`) into a `SqlSnippetSource`. Only the explicit + * `'logs'` opts a new snippet into the logs backend; anything else — including an + * absent param — is a database snippet, keeping database the safe default. + */ +export function parseSqlSnippetSource(raw: string | undefined): SqlSnippetSource { + return raw === 'logs' ? 'logs' : 'database' +} + /** * An ISO-8601 datetime proven valid at construction via a dayjs parse. Absolute * log ranges carry these instead of raw strings so an unvalidated datetime can @@ -135,6 +145,22 @@ export function logDateRangeToDatePickerValue(range: LogDateRange): DatePickerVa return { from: range.from, to: range.to, isHelper: false } } +/** + * Structural equality for two log date ranges. Relative ranges match on amount + + * unit, NOT display text — "Last hour" and "Last 1 hour" render differently but + * are the same range, so comparing labels is unreliable. Absolute ranges match on + * their (validated) ISO endpoints. + */ +export function logDateRangesEqual(a: LogDateRange, b: LogDateRange): boolean { + if (a.kind === 'relative' && b.kind === 'relative') { + return a.last.amount === b.last.amount && a.last.unit === b.last.unit + } + if (a.kind === 'absolute' && b.kind === 'absolute') { + return a.from === b.from && a.to === b.to + } + return false +} + /** * Resolve a `LogDateRange` to concrete ISO endpoints for a run. Relative ranges * re-resolve against `now` (so "last hour" is always the hour before the run); diff --git a/apps/studio/components/interfaces/SQLEditor/usePrettifyQuery.ts b/apps/studio/components/interfaces/SQLEditor/usePrettifyQuery.ts index 9a70c333034..8e287272600 100644 --- a/apps/studio/components/interfaces/SQLEditor/usePrettifyQuery.ts +++ b/apps/studio/components/interfaces/SQLEditor/usePrettifyQuery.ts @@ -24,9 +24,12 @@ export function usePrettifyQuery({ id, isDiffOpen }: { id: string; isDiffOpen: b const state = getSqlEditorV2StateSnapshot() const snippet = state.snippets[id] + // pg formatting can mangle ClickHouse syntax (backtick identifiers), so + // Prettify is a no-op for logs snippets — the UI also hides the affordance. + if (snippet?.snippet.type === 'log_sql') return + if (editor.isReady() && project) { - const fallback = - snippet?.snippet.type === 'log_sql' ? undefined : snippet?.snippet.content?.unchecked_sql + const fallback = snippet?.snippet.content?.unchecked_sql const sql = editor.getSql(fallback) if (sql === undefined) return diff --git a/apps/studio/components/interfaces/SQLEditor/useRunSource.ts b/apps/studio/components/interfaces/SQLEditor/useRunSource.ts index 9bccf7fb62b..aa7cbf05b6f 100644 --- a/apps/studio/components/interfaces/SQLEditor/useRunSource.ts +++ b/apps/studio/components/interfaces/SQLEditor/useRunSource.ts @@ -1,6 +1,13 @@ +import { useParams } from 'common' import { useMemo } from 'react' -import { DEFAULT_LOG_DATE_RANGE, type QuerySource } from './querySource' +import { + DEFAULT_LOG_DATE_RANGE, + getSnippetSource, + parseSqlSnippetSource, + type QuerySource, + type SqlSnippetSource, +} from './querySource' import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state' import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state' @@ -10,18 +17,26 @@ import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state * the user hasn't picked one); every other snippet targets the database. Source * is derived from the snippet's content type and is NOT flag-gated here — a * URL-opened logs snippet routes correctly even with the feature flag off. + * + * Before the snippet exists in the store (a fresh `/sql/new` tab, materialized + * lazily on the first keystroke) the source falls back to the `?source=` URL + * param, so a "New logs query" tab shows the logs toolbar and range picker + * immediately rather than flashing the database controls until the user types. */ export function useRunSource(id: string): QuerySource { + const { source: sourceParam } = useParams() const snapV2 = useSqlEditorV2StateSnapshot() const sessionSnap = useSqlEditorSessionSnapshot() - const snippetType = snapV2.snippets[id]?.snippet.type + const snippet = snapV2.snippets[id]?.snippet + const source: SqlSnippetSource = + snippet !== undefined ? getSnippetSource(snippet) : parseSqlSnippetSource(sourceParam) const logRange = sessionSnap.logRange[id] return useMemo(() => { - if (snippetType === 'log_sql') { + if (source === 'logs') { return { type: 'logs', dateRange: logRange ?? DEFAULT_LOG_DATE_RANGE } } return { type: 'database' } - }, [snippetType, logRange]) + }, [source, logRange]) } diff --git a/apps/studio/components/interfaces/SQLEditor/useSnippetEditor.ts b/apps/studio/components/interfaces/SQLEditor/useSnippetEditor.ts index 76c5a139296..f7bfd296d9c 100644 --- a/apps/studio/components/interfaces/SQLEditor/useSnippetEditor.ts +++ b/apps/studio/components/interfaces/SQLEditor/useSnippetEditor.ts @@ -2,6 +2,7 @@ import { useParams } from 'common' import { useRouter } from 'next/router' import { useEffect, useEffectEvent } from 'react' +import { parseSqlSnippetSource } from './querySource' import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useProfile } from '@/lib/profile' @@ -25,7 +26,7 @@ import { useTabsStateSnapshot } from '@/state/tabs' export function useSnippetEditor({ id, snippetName }: { id: string; snippetName: string }) { const router = useRouter() const { profile } = useProfile() - const { ref, content } = useParams() + const { ref, content, source } = useParams() const { data: project } = useSelectedProjectQuery() const snapV2 = useSqlEditorV2StateSnapshot() @@ -45,6 +46,9 @@ export function useSnippetEditor({ id, snippetName }: { id: string; snippetName: sql: value, owner_id: profile?.id, project_id: project?.id, + // A creation entry (e.g. "New logs query") threads the target backend + // through `/sql/new?source=`; default to a database snippet otherwise. + source: parseSqlSnippetSource(source), }) snapV2.addSnippet({ projectRef: ref, snippet: newSnippet }) // When the editor was seeded from a `content` deep-link, replace rather diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx index a27febc9d52..0fad5971029 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx @@ -1,7 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useDebounce } from '@uidotdev/usehooks' import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common' -import { FilePlus, FolderPlus, Plus, X } from 'lucide-react' +import { FilePlus, FolderPlus, Plus, ScrollText, X } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -24,6 +24,7 @@ import { import { SearchList } from './SQLEditorNavV2/SearchList' import { SQLEditorNav } from './SQLEditorNavV2/SQLEditorNav' +import { type SqlSnippetSource } from '@/components/interfaces/SQLEditor/querySource' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useLocalStorage } from '@/hooks/misc/useLocalStorage' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' @@ -39,6 +40,9 @@ export const SQLEditorMenu = () => { const snapV2 = useSqlEditorV2StateSnapshot() const topForPostgres = useFlag('topForPostgres') + const sqlEditorLogsSource = useFlag('sqlEditorLogsSource') + const otelLegacyLogs = useFlag('otelLegacyLogs') + const canCreateLogsSnippet = sqlEditorLogsSource && otelLegacyLogs const [search, setSearch] = useState('') const [showSearch, setShowSearch] = useState(false) @@ -66,7 +70,7 @@ export const SQLEditorMenu = () => { snapV2.addNewFolder({ projectRef: ref }) } - const handleNewQuery = async () => { + const handleNewQuery = async (source: SqlSnippetSource = 'database') => { if (!ref) return console.error('Project ref is required') if (!project) return console.error('Project is required') if (!profile) return console.error('Profile is required') @@ -74,7 +78,8 @@ export const SQLEditorMenu = () => { return toast('Your queries will not be saved as you do not have sufficient permissions') } try { - router.push(`/project/${ref}/sql/new?skip=true`) + const suffix = source === 'logs' ? '&source=logs' : '' + router.push(`/project/${ref}/sql/new?skip=true${suffix}`) setSearch('') setShowSearch(false) } catch (error: any) { @@ -156,6 +161,12 @@ export const SQLEditorMenu = () => { Create a new snippet + {canCreateLogsSnippet && ( + handleNewQuery('logs')}> + + Create a new logs query + + )} createNewFolder()}> Create a new folder diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx index 828f99b7687..21ae2a5e129 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx @@ -28,6 +28,7 @@ import { TreeViewItem, } from 'ui' +import { getSnippetSource } from '@/components/interfaces/SQLEditor/querySource' import { createSqlSnippetSkeletonV2 } from '@/components/interfaces/SQLEditor/SQLEditor.utils' import { getContentById, getSqlSnippetById } from '@/data/content/content-id-query' import { useSQLSnippetFolderContentsQuery } from '@/data/content/sql-folder-contents-query' @@ -225,6 +226,7 @@ export const SQLEditorTreeViewItem = ({ sql, owner_id: profile?.id, project_id: project?.id, + source: getSnippetSource(snippet), }) snapV2.addSnippet({ projectRef, snippet: snippetCopy }) @@ -420,16 +422,18 @@ export const SQLEditorTreeViewItem = ({ {isFavorite ? 'Remove from' : 'Add to'} favorites )} - {onSelectDownload !== undefined && IS_PLATFORM && ( - onSelectDownload()} - onFocusCapture={(e) => e.stopPropagation()} - > - - Export query - - )} + {onSelectDownload !== undefined && + IS_PLATFORM && + getSnippetSource(element.metadata) !== 'logs' && ( + onSelectDownload()} + onFocusCapture={(e) => e.stopPropagation()} + > + + Export query + + )} {onSelectDelete !== undefined && isOwner && ( <> From bf5a729f2d68dd64c606c078237db5ca2f47b2fe Mon Sep 17 00:00:00 2001 From: Steven Eubank <47563310+smeubank@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:19:00 +0200 Subject: [PATCH 125/141] docs(telemetry): rename section and restructure as Monitoring and Debugging (#48243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Renames the **Telemetry** nav section to **Monitoring and Debugging** (nav label + sidebar title) - Rewrites the section overview (`telemetry.mdx`) as a clean navigation page using `ContentListings` — three panels (Debugging / Monitoring / AI & automation) with no how-to prose - Adds new `telemetry.data.ts` content-listings data file with three groups registered in `index.ts` - Adds a new **Debugging** guide (`debugging.mdx`) — request-stack model, symptom-to-layer router with troubleshooting links for every service, logging guidance - Adds cross-links between `debugging.mdx`, `logs.mdx`, and `advanced-log-filtering.mdx` - Adds a new **AI agents and MCP** page (`ai-agents.mdx`) — MCP tools table, `get_logs` usage, debugging skill workflow - Restructures sidebar into three groups: **Debugging** / **Monitoring** / **AI & automation** ## Motivation - No central entry point existed for debugging — content was scattered across products with no index - The overview page had almost no links for agents to follow - The section name "Telemetry" caused confusion (also used for CLI usage telemetry) - Unblocks the `supabase` debugging skill, which routes agents to this section as its source of truth ## Test plan - [ ] `/docs/guides/telemetry` — three ContentListings panels render, no prose how-to text - [ ] `/docs/guides/telemetry.md` (markdown) — clean link list, navigable by LLMs - [ ] `/docs/guides/telemetry/debugging` — renders correctly, symptom table links resolve - [ ] `/docs/guides/telemetry/ai-agents` — new page renders correctly - [ ] Sidebar shows 3 groups: Debugging / Monitoring / AI & automation - [ ] All cross-links between debugging, logs, and advanced-log-filtering resolve ## Summary by CodeRabbit ## Summary - **New Features** - Added new documentation coverage for AI agent–assisted monitoring and debugging, including an observability-driven troubleshooting workflow. - **Documentation** - Updated the “Telemetry” area to “Monitoring and Debugging” with a refreshed landing page and reorganized sections (Debugging, Monitoring, and AI). - Revised the debugging and logs guides to improve step-by-step guidance and highlight advanced log filtering. - **Navigation** - Renamed and restructured the top-level navigation entry to reflect the new Monitoring and Debugging content layout. --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Jeremias Menichelli --- .../[[...slug]]/page.tsx | 8 ++- .../monitoring-and-debugging/layout.tsx | 5 ++ apps/docs/app/guides/telemetry/layout.tsx | 5 -- .../docs/components/MetricsStackCards.data.ts | 6 +- .../NavigationMenu.constants.ts | 54 ++++++++------- .../NavigationMenu/NavigationMenu.utils.ts | 7 +- .../guides/database/connection-management.mdx | 4 +- .../guides/database/extensions/pgaudit.mdx | 2 +- .../manual-replication-monitoring.mdx | 4 +- .../guides/monitoring-and-debugging.mdx | 9 +++ .../advanced-log-filtering.mdx | 0 .../client-side-tracing.mdx | 2 +- .../debugging.mdx | 23 +++++-- .../log-drains.mdx | 6 +- .../log-field-reference.mdx | 2 +- .../logs.mdx | 12 ++++ .../metrics.mdx | 2 +- .../metrics/grafana-cloud.mdx | 0 .../metrics/grafana-self-hosted.mdx | 0 .../metrics/vendor-agnostic.mdx | 0 .../reports.mdx | 0 .../sentry-monitoring.mdx | 0 .../guides/platform/billing-on-supabase.mdx | 2 +- .../platform/postgres-connection-logging.mdx | 4 +- .../content/guides/platform/read-replicas.mdx | 4 +- .../read-replicas/getting-started.mdx | 2 +- .../guides/security/platform-audit-logs.mdx | 2 +- .../content/guides/storage/cdn/metrics.mdx | 2 +- .../content/guides/storage/debugging/logs.mdx | 2 +- apps/docs/content/guides/telemetry.mdx | 13 ---- .../troubleshooting/exhaust-disk-io.mdx | 2 +- .../content/troubleshooting/exhaust-ram.mdx | 2 +- .../content/troubleshooting/exhaust-swap.mdx | 2 +- .../failed-to-retrieve-tables.mdx | 2 +- ...n-terminated-due-to-connection-timeout.mdx | 2 +- .../troubleshooting/high-cpu-usage.mdx | 2 +- ...t-and-explore-the-postgres-logs-OuCIOj.mdx | 2 +- .../how-to-view-database-metrics-uqf2z_.mdx | 2 +- ...ting-supabase-grafana-io-charts-MUynDR.mdx | 2 +- ...monitor-supavisor-postgres-connections.mdx | 2 +- ...-query-performance-with-indexes-q8PoC9.mdx | 2 +- .../supabase-grafana-memory-charts.mdx | 2 +- .../troubleshooting/supavisor-faq-YyP5tI.mdx | 2 +- apps/docs/data/content-listings/index.ts | 3 + .../data/content-listings/log-drains.data.ts | 18 ++--- .../data/content-listings/telemetry.data.ts | 65 +++++++++++++++++++ apps/docs/features/docs/GuidesMdx.utils.tsx | 2 +- apps/docs/lib/breadcrumbs.ts | 2 +- apps/docs/lib/content-listings.test.ts | 2 +- apps/www/lib/redirects.js | 5 ++ 50 files changed, 202 insertions(+), 103 deletions(-) rename apps/docs/app/guides/{telemetry => monitoring-and-debugging}/[[...slug]]/page.tsx (73%) create mode 100644 apps/docs/app/guides/monitoring-and-debugging/layout.tsx delete mode 100644 apps/docs/app/guides/telemetry/layout.tsx create mode 100644 apps/docs/content/guides/monitoring-and-debugging.mdx rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/advanced-log-filtering.mdx (100%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/client-side-tracing.mdx (94%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/debugging.mdx (92%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/log-drains.mdx (96%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/log-field-reference.mdx (92%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/logs.mdx (97%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/metrics.mdx (94%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/metrics/grafana-cloud.mdx (100%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/metrics/grafana-self-hosted.mdx (100%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/metrics/vendor-agnostic.mdx (100%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/reports.mdx (100%) rename apps/docs/content/guides/{telemetry => monitoring-and-debugging}/sentry-monitoring.mdx (100%) delete mode 100644 apps/docs/content/guides/telemetry.mdx create mode 100644 apps/docs/data/content-listings/telemetry.data.ts diff --git a/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx b/apps/docs/app/guides/monitoring-and-debugging/[[...slug]]/page.tsx similarity index 73% rename from apps/docs/app/guides/telemetry/[[...slug]]/page.tsx rename to apps/docs/app/guides/monitoring-and-debugging/[[...slug]]/page.tsx index 0aa9feceb0d..94df4a9b279 100644 --- a/apps/docs/app/guides/telemetry/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/monitoring-and-debugging/[[...slug]]/page.tsx @@ -11,15 +11,17 @@ type Params = { slug?: string[] } const MonitoringTroubleshootingGuidePage = async (props: { params: Promise }) => { const params = await props.params - const slug = ['telemetry', ...(params.slug ?? [])] + const slug = ['monitoring-and-debugging', ...(params.slug ?? [])] const data = await getGuidesMarkdown(slug) return } -const generateStaticParams = !IS_DEV ? genGuidesStaticParams('telemetry') : getEmptyArray +const generateStaticParams = !IS_DEV + ? genGuidesStaticParams('monitoring-and-debugging') + : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => - getGuidesMarkdown(['telemetry', ...(params.slug ?? [])]) + getGuidesMarkdown(['monitoring-and-debugging', ...(params.slug ?? [])]) ) export default MonitoringTroubleshootingGuidePage diff --git a/apps/docs/app/guides/monitoring-and-debugging/layout.tsx b/apps/docs/app/guides/monitoring-and-debugging/layout.tsx new file mode 100644 index 00000000000..8c99a320c38 --- /dev/null +++ b/apps/docs/app/guides/monitoring-and-debugging/layout.tsx @@ -0,0 +1,5 @@ +import Layout from '~/layouts/guides' + +export default async function MonitoringAndDebugging({ children }: { children: React.ReactNode }) { + return {children} +} diff --git a/apps/docs/app/guides/telemetry/layout.tsx b/apps/docs/app/guides/telemetry/layout.tsx deleted file mode 100644 index e2fe19972d4..00000000000 --- a/apps/docs/app/guides/telemetry/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import Layout from '~/layouts/guides' - -export default async function Telemetry({ children }: { children: React.ReactNode }) { - return {children} -} diff --git a/apps/docs/components/MetricsStackCards.data.ts b/apps/docs/components/MetricsStackCards.data.ts index 637af231728..d9217a23019 100644 --- a/apps/docs/components/MetricsStackCards.data.ts +++ b/apps/docs/components/MetricsStackCards.data.ts @@ -13,7 +13,7 @@ export const metricsStackOptions: MetricsStackOption[] = [ title: 'Grafana Cloud (SaaS)', description: 'Use Grafana Cloud’s managed Prometheus (works on Free + Pro tiers) and import the Supabase dashboard without running any infrastructure.', - href: '/guides/telemetry/metrics/grafana-cloud', + href: '/guides/monitoring-and-debugging/metrics/grafana-cloud', iconKind: 'grafana', iconColor: '#F05A28', iconBg: 'rgba(240,90,40,0.1)', @@ -23,7 +23,7 @@ export const metricsStackOptions: MetricsStackOption[] = [ title: 'Grafana + self-hosted Prometheus', description: 'Run Prometheus yourself following the official installation guidance and pair it with Grafana plus our dashboard JSON and alert pack.', - href: '/guides/telemetry/metrics/grafana-self-hosted', + href: '/guides/monitoring-and-debugging/metrics/grafana-self-hosted', iconKind: 'grafana', iconColor: '#F05A28', iconBg: 'rgba(240,90,40,0.1)', @@ -43,7 +43,7 @@ export const metricsStackOptions: MetricsStackOption[] = [ title: 'Vendor-agnostic / BYO Prometheus', description: 'Connect AWS AMP, Grafana Mimir, VictoriaMetrics, or any Prometheus-compatible SaaS with the same scrape job pattern.', - href: '/guides/telemetry/metrics/vendor-agnostic', + href: '/guides/monitoring-and-debugging/metrics/vendor-agnostic', iconKind: 'flame', iconColor: '#0BA678', iconBg: 'rgba(11,166,120,0.1)', diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index cb2ec0b8c1d..96e6899ff95 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -212,9 +212,9 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ level: 'security', }, { - label: 'Telemetry', + label: 'Monitoring and Debugging', icon: 'telemetry', - href: '/guides/telemetry' as `/${string}`, + href: '/guides/monitoring-and-debugging' as `/${string}`, level: 'telemetry', }, { @@ -2983,57 +2983,59 @@ export const platform: NavMenuConstant = { export const telemetry: NavMenuConstant = { icon: 'telemetry', - title: 'Telemetry', - url: '/guides/telemetry', + title: 'Monitoring and Debugging', + url: '/guides/monitoring-and-debugging', items: [ - { name: 'Overview', url: '/guides/telemetry' }, + { name: 'Overview', url: '/guides/monitoring-and-debugging' }, { - name: 'Logging & observability', + name: 'Debugging', url: undefined, items: [ { - name: 'Logging', - url: '/guides/telemetry/logs' as `/${string}`, + name: 'Debugging guide', + url: '/guides/monitoring-and-debugging/debugging' as `/${string}`, }, { - name: 'Debugging', - url: '/guides/telemetry/debugging' as `/${string}`, + name: 'Logging', + url: '/guides/monitoring-and-debugging/logs' as `/${string}`, }, { name: 'Advanced log filtering', - url: '/guides/telemetry/advanced-log-filtering' as `/${string}`, + url: '/guides/monitoring-and-debugging/advanced-log-filtering' as `/${string}`, }, { name: 'Logs field reference', - url: '/guides/telemetry/log-field-reference' as `/${string}`, + url: '/guides/monitoring-and-debugging/log-field-reference' as `/${string}`, }, + ], + }, + { + name: 'Monitoring', + url: undefined, + items: [ { name: 'Log drains', - url: '/guides/telemetry/log-drains' as `/${string}`, - }, - { - name: 'Tracing with the JS SDK', - url: '/guides/telemetry/client-side-tracing' as `/${string}`, + url: '/guides/monitoring-and-debugging/log-drains' as `/${string}`, }, { name: 'Reports', - url: '/guides/telemetry/reports' as `/${string}`, + url: '/guides/monitoring-and-debugging/reports' as `/${string}`, }, { name: 'Metrics', - url: '/guides/telemetry/metrics' as `/${string}`, + url: '/guides/monitoring-and-debugging/metrics' as `/${string}`, items: [ { name: 'Overview', - url: '/guides/telemetry/metrics' as `/${string}`, + url: '/guides/monitoring-and-debugging/metrics' as `/${string}`, }, { name: 'Grafana Cloud', - url: '/guides/telemetry/metrics/grafana-cloud' as `/${string}`, + url: '/guides/monitoring-and-debugging/metrics/grafana-cloud' as `/${string}`, }, { name: 'Grafana self-hosted', - url: '/guides/telemetry/metrics/grafana-self-hosted' as `/${string}`, + url: '/guides/monitoring-and-debugging/metrics/grafana-self-hosted' as `/${string}`, }, { name: 'Datadog', @@ -3041,13 +3043,17 @@ export const telemetry: NavMenuConstant = { }, { name: 'Vendor-agnostic setup', - url: '/guides/telemetry/metrics/vendor-agnostic' as `/${string}`, + url: '/guides/monitoring-and-debugging/metrics/vendor-agnostic' as `/${string}`, }, ], }, { name: 'Sentry integration', - url: '/guides/telemetry/sentry-monitoring' as `/${string}`, + url: '/guides/monitoring-and-debugging/sentry-monitoring' as `/${string}`, + }, + { + name: 'Tracing with the JS SDK', + url: '/guides/monitoring-and-debugging/client-side-tracing' as `/${string}`, }, ], }, diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts index 556d87971a0..a44f85f4fba 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.utils.ts @@ -1,10 +1,11 @@ 'use client' -import { usePathname } from 'next/navigation' -import { useEffect, useState } from 'react' import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' import type { ICommonItem } from '~/components/reference/Reference.types' import type { Json } from '~/features/helpers.types' +import { usePathname } from 'next/navigation' +import { useEffect, useState } from 'react' + import { menuState } from '../../../hooks/useMenuState' export function getPathWithoutHash(relativePath: string) { @@ -134,7 +135,7 @@ export const getMenuId = (pathname: string | null) => { return MenuId.LocalDevelopment case pathname.startsWith('ai-tools'): return MenuId.AiTools - case pathname.startsWith('telemetry'): + case pathname.startsWith('monitoring-and-debugging'): return MenuId.Telemetry case pathname.startsWith('platform'): return MenuId.Platform diff --git a/apps/docs/content/guides/database/connection-management.mdx b/apps/docs/content/guides/database/connection-management.mdx index 2810bbe7f4b..b94395c6338 100644 --- a/apps/docs/content/guides/database/connection-management.mdx +++ b/apps/docs/content/guides/database/connection-management.mdx @@ -47,11 +47,11 @@ For Teams and Enterprise plans, Supabase provides Advanced Telemetry charts dire This chart helps you monitor connection pool usage, identify connection leaks, and plan capacity. It also shows a reference line for your compute size's maximum connection limit. -For more details on using these monitoring charts, see the [Reports guide](/docs/guides/telemetry/reports#advanced-telemetry). +For more details on using these monitoring charts, see the [Reports guide](/docs/guides/monitoring-and-debugging/reports#advanced-telemetry). #### Grafana Dashboard -Supabase offers a Grafana Dashboard that records and visualizes over 200 project metrics, including connections. For setup instructions, check the [metrics docs](/docs/guides/telemetry/metrics). +Supabase offers a Grafana Dashboard that records and visualizes over 200 project metrics, including connections. For setup instructions, check the [metrics docs](/docs/guides/monitoring-and-debugging/metrics). Its "Client Connections" graph displays connections for both Supavisor and Postgres ![client connection graph](/docs/img/database/grafana-connections.png) diff --git a/apps/docs/content/guides/database/extensions/pgaudit.mdx b/apps/docs/content/guides/database/extensions/pgaudit.mdx index a451ef2d138..8416bf41c1c 100644 --- a/apps/docs/content/guides/database/extensions/pgaudit.mdx +++ b/apps/docs/content/guides/database/extensions/pgaudit.mdx @@ -391,5 +391,5 @@ PGAudit's [official documentation](https://www.pgaudit.org) focuses on system an - [Official `PGAudit` documentation](https://www.pgaudit.org) - [Database Function Logging](/docs/guides/database/functions#general-logging) -- [Supabase Logging](/docs/guides/telemetry/logs) +- [Supabase Logging](/docs/guides/monitoring-and-debugging/logs) - [Self-Hosting Logs](/docs/reference/self-hosting-analytics/introduction) diff --git a/apps/docs/content/guides/database/replication/manual-replication-monitoring.mdx b/apps/docs/content/guides/database/replication/manual-replication-monitoring.mdx index 0e9da551f2b..8edf982a0c4 100644 --- a/apps/docs/content/guides/database/replication/manual-replication-monitoring.mdx +++ b/apps/docs/content/guides/database/replication/manual-replication-monitoring.mdx @@ -8,12 +8,12 @@ sidebar_label: 'Monitoring' Monitoring replication lag is important and there are 3 ways to do this: -1. Dashboard - In [Reports](/docs/guides/telemetry/reports), you can view the replication lag of your project +1. Dashboard - In [Reports](/docs/guides/monitoring-and-debugging/reports), you can view the replication lag of your project 2. Database - - pg_stat_subscription (subscriber) - if PID is null, then the subscription is not active - pg_stat_subscription_stats - look here for error_count to see if there were issues applying or syncing (if yes, check the logs for why) - pg_replication_slots - use this to check if the slot is active and you can also calculate the lag from here -3. [Metrics](/docs/guides/telemetry/metrics) - Using the prometheus endpoint for your project +3. [Metrics](/docs/guides/monitoring-and-debugging/metrics) - Using the prometheus endpoint for your project - replication_slots_max_lag_bytes - this is the more important one - pg_stat_replication_replay_lag - lag to replay WAL files from the source DB on the target DB (throttled by disk or high activity) - pg_stat_replication_send_lag - lag in sending WAL files from the source DB (a high lag means that the publisher is not being asked to send new WAL files OR network issues) diff --git a/apps/docs/content/guides/monitoring-and-debugging.mdx b/apps/docs/content/guides/monitoring-and-debugging.mdx new file mode 100644 index 00000000000..f6b732836ae --- /dev/null +++ b/apps/docs/content/guides/monitoring-and-debugging.mdx @@ -0,0 +1,9 @@ +--- +title: Monitoring and Debugging +--- + +Monitor your project, debug errors, and understand what's happening across the Supabase stack. + + + + diff --git a/apps/docs/content/guides/telemetry/advanced-log-filtering.mdx b/apps/docs/content/guides/monitoring-and-debugging/advanced-log-filtering.mdx similarity index 100% rename from apps/docs/content/guides/telemetry/advanced-log-filtering.mdx rename to apps/docs/content/guides/monitoring-and-debugging/advanced-log-filtering.mdx diff --git a/apps/docs/content/guides/telemetry/client-side-tracing.mdx b/apps/docs/content/guides/monitoring-and-debugging/client-side-tracing.mdx similarity index 94% rename from apps/docs/content/guides/telemetry/client-side-tracing.mdx rename to apps/docs/content/guides/monitoring-and-debugging/client-side-tracing.mdx index e2e8250c708..49fd270ab24 100644 --- a/apps/docs/content/guides/telemetry/client-side-tracing.mdx +++ b/apps/docs/content/guides/monitoring-and-debugging/client-side-tracing.mdx @@ -73,7 +73,7 @@ Once trace context is flowing through, the `trace_id` appears in: - **API Gateway logs** — every request to PostgREST, Auth, Storage, and Realtime - **Edge Function logs** — invocations and any structured logs emitted from within the function -If you forward Supabase logs to a third-party backend via [Log Drains](/docs/guides/telemetry/log-drains), you can join Supabase logs to your own client and server traces using the shared `trace_id`. This is especially useful for self-hosted setups where you already operate your own OpenTelemetry collector — Supabase logs become first-class citizens in your existing tracing UI. +If you forward Supabase logs to a third-party backend via [Log Drains](/docs/guides/monitoring-and-debugging/log-drains), you can join Supabase logs to your own client and server traces using the shared `trace_id`. This is especially useful for self-hosted setups where you already operate your own OpenTelemetry collector — Supabase logs become first-class citizens in your existing tracing UI. ## Using a vendor tracing SDK diff --git a/apps/docs/content/guides/telemetry/debugging.mdx b/apps/docs/content/guides/monitoring-and-debugging/debugging.mdx similarity index 92% rename from apps/docs/content/guides/telemetry/debugging.mdx rename to apps/docs/content/guides/monitoring-and-debugging/debugging.mdx index 1711e999833..0c84be99fce 100644 --- a/apps/docs/content/guides/telemetry/debugging.mdx +++ b/apps/docs/content/guides/monitoring-and-debugging/debugging.mdx @@ -1,22 +1,22 @@ --- id: 'debugging' -title: 'Debugging' +title: 'Debugging guide' description: 'Isolate and fix Supabase issues by reading the error, isolating the failing layer, and gathering evidence from logs.' --- Debug by evidence, not by guessing. A Supabase error almost always surfaces at one layer but originates at another, so the fastest path to a fix is finding _where_ the problem is, not pattern-matching the symptom. Retrying a failed request rarely helps; isolating the layer does. -## The debugging loop +## Follow these debugging steps Work through these steps in order, skipping straight to a fix before you have evidence for the cause is the most common way to waste time on a bug. 1. **Reproduce the issue and read the error precisely.** Capture the exact status code, the error code, and the full message, not a paraphrase. A `401` is not a `403`; `PGRST002` is not `PGRST106`; a Postgres `SQLSTATE` such as `42501`, `42P01`, or `23505` points at the exact failure. The precise error is your strongest clue. If you're using `supabase-js`, remember that errors are **returned, not thrown**, check the `error` field in the `{ data, error }` response object. Make sure your code inspects `error` — a swallowed error is why many bugs look like "nothing happened". 2. **Locate the failing layer.** Use the request stack below. The status code and error code usually name the layer for you. -3. **Gather evidence for that layer.** Query its logs, run the security and performance advisors, and inspect the schema. Logs are the primary tool, and the layer you identified in the previous step tells you which log source to query. See [Reading logs](#reading-logs) below. +3. **Gather evidence for that layer.** Query its logs, run the security and performance advisors, and inspect the schema. Logs are the primary tool, and the layer you identified in the previous step tells you which log source to query. See [Read the logs](#read-the-logs) below. 4. **Isolate the cause** using the troubleshooting guide for that layer (see [Find the guide for your symptom](#find-the-guide-for-your-symptom) below). Confirm your hypothesis against the evidence before you act. Most Supabase issues trace back to a small, known set of causes, and the guide explains how to tell them apart. 5. **Apply the fix, then verify.** Re-run the exact operation that failed and confirm it now succeeds, and that the corresponding log line is clean. A fix you haven't re-run is still a guess. If a couple of attempts don't resolve it, stop and gather more evidence rather than repeating the same change. -## The Supabase request stack +## Check the request stack A request from a client passes through several layers before it reaches your data. Errors propagate upward, so the layer that _reports_ an error is often not the layer that _caused_ it. @@ -44,7 +44,7 @@ A permission error or an unexpectedly empty result at the API layer is often a P -## Reading logs +## Read the logs Once you know the layer, query that layer's log source directly rather than scanning everything. Pick one `source`, bound the time window, and select only the fields you need. @@ -52,7 +52,7 @@ When a query comes up empty, widen along an anchor, such as a timestamp, request A wide, unfiltered query across every source buries the one line you need and, on paid projects, costs more in scanned data. -The [Logging guide](/docs/guides/telemetry/logs) covers the Logs Explorer, the available log sources, and how to write queries against them. +The [Logging guide](/docs/guides/monitoring-and-debugging/logs) covers the Logs Explorer, the available log sources, and how to write queries against them. ## Find the guide for your symptom @@ -71,8 +71,17 @@ Supabase updates these troubleshooting guides continuously, so treat this table | Realtime `TIMED_OUT`; `TooManyChannels`; silent disconnect; missed database changes; broadcast-from-DB warning; heartbeats | Realtime → `realtime_logs` | [TIMED_OUT](/docs/guides/troubleshooting/realtime-connections-timed_out-status) · [TooManyChannels](/docs/guides/troubleshooting/realtime-too-many-channels-error) · [Silent disconnects](/docs/guides/troubleshooting/realtime-handling-silent-disconnections-in-backgrounded-applications-592794) · [Broadcast warning](/docs/guides/troubleshooting/realtime-warn-sending-broadcast-message) · [Heartbeats](/docs/guides/troubleshooting/realtime-heartbeat-messages) · [Logger](/docs/guides/troubleshooting/realtime-debugging-with-logger) | | Upload or list fails; public bucket inaccessible; `relation "objects" does not exist`; file size limit; folder or RLS issue | Storage → `storage_logs`, `postgres_logs` | [Public bucket upload/list](/docs/guides/troubleshooting/why-cant-i-uploadlistetc-my-public-bucket-Z6CmGt) · [403 RLS on upload](/docs/guides/troubleshooting/storage-error-403-forbidden-new-row-violates-row-level-security-policy-on-upload-a94384) · [relation objects does not exist](/docs/guides/troubleshooting/relation-objects-does-not-exist-error-during-storage-uploads-8f21f0) · [File size limits](/docs/guides/troubleshooting/upload-file-size-restrictions-Y4wQLT) · [Folder ops / hierarchical RLS](/docs/guides/troubleshooting/supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d) | | Webhook not firing; `pg_cron` job not running; `pg_net` queue stuck; `42501 ... http_request_queue` | Database jobs → `postgres_logs` | [Webhook debugging](/docs/guides/troubleshooting/webhook-debugging-guide-M8sk47) · [pg_cron debugging](/docs/guides/troubleshooting/pgcron-debugging-guide-n1KTaz) · [42501 http_request_queue](/docs/guides/troubleshooting/42501--permission-denied-for-table-httprequestqueue-KnozmQ) | -| Reading or querying logs; interpreting Postgres logs; finding API errors in logs; reading metrics | Diagnostics → any log source | [Logging guide](/docs/guides/telemetry/logs) · [Interpret Postgres logs](/docs/guides/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj) · [API errors in logs](/docs/guides/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9) · [Logging levels](/docs/guides/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm) · [View database metrics](/docs/guides/troubleshooting/how-to-view-database-metrics-uqf2z_) | +| Reading or querying logs; interpreting Postgres logs; finding API errors in logs; reading metrics | Diagnostics → any log source | [Logging guide](/docs/guides/monitoring-and-debugging/logs) · [Interpret Postgres logs](/docs/guides/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj) · [API errors in logs](/docs/guides/troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9) · [Logging levels](/docs/guides/troubleshooting/understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm) · [View database metrics](/docs/guides/troubleshooting/how-to-view-database-metrics-uqf2z_) | For query performance and schema-design questions such as indexing, `EXPLAIN`, N+1 queries, or partitioning, see the [Postgres guides](/docs/guides/database/overview). Debugging is complete only once you've re-run the failing operation, confirmed it succeeds, and checked that the layer's logs show a clean result. + +## Per-product debugging + +Each Supabase product has its own debugging resources. Use these as a starting point when the error originates in a specific service. + +- [Database — Debugging and monitoring](/docs/guides/database/inspect) +- [Auth — Error codes](/docs/guides/auth/debugging/error-codes) +- [Storage — Debugging](/docs/guides/storage/debugging/logs) +- [Edge Functions — Local debugging](/docs/guides/functions/debugging-tools) diff --git a/apps/docs/content/guides/telemetry/log-drains.mdx b/apps/docs/content/guides/monitoring-and-debugging/log-drains.mdx similarity index 96% rename from apps/docs/content/guides/telemetry/log-drains.mdx rename to apps/docs/content/guides/monitoring-and-debugging/log-drains.mdx index 3934c7b5d7d..8ca351c8bbd 100644 --- a/apps/docs/content/guides/telemetry/log-drains.mdx +++ b/apps/docs/content/guides/monitoring-and-debugging/log-drains.mdx @@ -9,7 +9,7 @@ Log drains send all logs of the Supabase stack to one or more desired destinatio ## What you can do with log drains - Route Supabase logs (Postgres, Auth, Storage, Edge Functions, and more) to any observability platform. -- Combine Supabase logs with application-level traces — see [Tracing with the JS SDK](/docs/guides/telemetry/client-side-tracing) to extend your traces into Supabase. +- Combine Supabase logs with application-level traces — see [Tracing with the JS SDK](/docs/guides/monitoring-and-debugging/client-side-tracing) to extend your traces into Supabase. - Archive logs to S3 for long-term retention and compliance. - Build alerts and dashboards on top of Supabase log data in your preferred vendor. @@ -335,5 +335,5 @@ Logs are forwarded to a remote Syslog receiver using TCP or TLS, adhering to [RF ## Additional resources - [Log Drains pricing breakdown](/docs/guides/platform/manage-your-usage/log-drains) — cost per drain, per million events, and egress charges. -- [Metrics API](/docs/guides/telemetry/metrics) — export Postgres performance metrics alongside your logs. -- [Tracing with the JS SDK](/docs/guides/telemetry/client-side-tracing) — instrument your application and combine traces with Supabase logs. +- [Metrics API](/docs/guides/monitoring-and-debugging/metrics) — export Postgres performance metrics alongside your logs. +- [Tracing with the JS SDK](/docs/guides/monitoring-and-debugging/client-side-tracing) — instrument your application and combine traces with Supabase logs. diff --git a/apps/docs/content/guides/telemetry/log-field-reference.mdx b/apps/docs/content/guides/monitoring-and-debugging/log-field-reference.mdx similarity index 92% rename from apps/docs/content/guides/telemetry/log-field-reference.mdx rename to apps/docs/content/guides/monitoring-and-debugging/log-field-reference.mdx index c7eb0fa1bfc..0f18f06eb86 100644 --- a/apps/docs/content/guides/telemetry/log-field-reference.mdx +++ b/apps/docs/content/guides/monitoring-and-debugging/log-field-reference.mdx @@ -4,7 +4,7 @@ title: 'Logs field reference' description: 'Supabase Logs field reference' --- -Refer to the full field reference for each available source below. To access each nested key, you need to perform the [necessary unnesting joins](/docs/guides/telemetry/advanced-log-filtering#unnesting-arrays) +Refer to the full field reference for each available source below. To access each nested key, you need to perform the [necessary unnesting joins](/docs/guides/monitoring-and-debugging/advanced-log-filtering#unnesting-arrays) {(logConstants) => ( diff --git a/apps/docs/content/guides/telemetry/logs.mdx b/apps/docs/content/guides/monitoring-and-debugging/logs.mdx similarity index 97% rename from apps/docs/content/guides/telemetry/logs.mdx rename to apps/docs/content/guides/monitoring-and-debugging/logs.mdx index 9b945fdf217..c63d3faf3ed 100644 --- a/apps/docs/content/guides/telemetry/logs.mdx +++ b/apps/docs/content/guides/monitoring-and-debugging/logs.mdx @@ -6,10 +6,22 @@ description: 'Getting started with Supabase Log Browser' The Supabase Platform includes a Logs Explorer that allows log tracing and debugging. Log retention is based on your [project's pricing plan](/pricing). For details on how Logs usage is billed, see [Manage Logs usage](/docs/guides/platform/manage-your-usage/logs). + + +If you are debugging a specific error or unexpected behavior, start with the [Debugging guide](/docs/guides/monitoring-and-debugging/debugging) — it routes you to the right log source based on your error code or symptom before you open the Logs Explorer. + + + ## Product logs Supabase provides a logging interface specific to each product. You can use regular expressions for keywords and patterns to search log event messages. You can also export and download the log events matching your query as a spreadsheet. + + +For regular expression filtering, structured-field queries, and field discovery techniques, see [Advanced log filtering](/docs/guides/monitoring-and-debugging/advanced-log-filtering). + + + {/* */} -If you are already ingesting your [project's metrics](/docs/guides/telemetry/metrics) into your own environment, you can also keep track of replication lag and set alarms with the `physical_replication_lag_physical_replica_lag_seconds` metric. +If you are already ingesting your [project's metrics](/docs/guides/monitoring-and-debugging/metrics) into your own environment, you can also keep track of replication lag and set alarms with the `physical_replication_lag_physical_replica_lag_seconds` metric. diff --git a/apps/docs/content/guides/security/platform-audit-logs.mdx b/apps/docs/content/guides/security/platform-audit-logs.mdx index 1ead782aa0d..47344d01696 100644 --- a/apps/docs/content/guides/security/platform-audit-logs.mdx +++ b/apps/docs/content/guides/security/platform-audit-logs.mdx @@ -46,7 +46,7 @@ Each Supabase user account also has access to [Account Audit logs](/dashboard/ac ## Accessing Audit Log Drains -Audit Log Drains can be configured under your [organization's audit log drains](/dashboard/org/_/audit-log-drains). For setup instructions and supported destinations, see the [Log Drains guide](/docs/guides/telemetry/log-drains). +Audit Log Drains can be configured under your [organization's audit log drains](/dashboard/org/_/audit-log-drains). For setup instructions and supported destinations, see the [Log Drains guide](/docs/guides/monitoring-and-debugging/log-drains). ## Limitations diff --git a/apps/docs/content/guides/storage/cdn/metrics.mdx b/apps/docs/content/guides/storage/cdn/metrics.mdx index f6da42077da..13818122f14 100644 --- a/apps/docs/content/guides/storage/cdn/metrics.mdx +++ b/apps/docs/content/guides/storage/cdn/metrics.mdx @@ -5,7 +5,7 @@ description: 'Learn how Supabase Storage caches objects with a CDN.' sidebar_label: 'CDN' --- -Cache hits can be determined via the `metadata.response.headers.cf_cache_status` key in our [Logs Explorer](/docs/guides/telemetry/logs#logs-explorer). Any value that corresponds to either `HIT`, `STALE`, `REVALIDATED`, or `UPDATING` is categorized as a cache hit. +Cache hits can be determined via the `metadata.response.headers.cf_cache_status` key in our [Logs Explorer](/docs/guides/monitoring-and-debugging/logs#logs-explorer). Any value that corresponds to either `HIT`, `STALE`, `REVALIDATED`, or `UPDATING` is categorized as a cache hit. The following example query will show the top cache misses from the `edge_logs`: ```sql diff --git a/apps/docs/content/guides/storage/debugging/logs.mdx b/apps/docs/content/guides/storage/debugging/logs.mdx index ec9e9a36b46..9470a55b25e 100644 --- a/apps/docs/content/guides/storage/debugging/logs.mdx +++ b/apps/docs/content/guides/storage/debugging/logs.mdx @@ -11,7 +11,7 @@ For more advanced filtering needs, use the [Logs Explorer](/dashboard/project/_/ -For more details on filtering the log tables, see [Advanced Log Filtering](/docs/guides/telemetry/advanced-log-filtering) +For more details on filtering the log tables, see [Advanced Log Filtering](/docs/guides/monitoring-and-debugging/advanced-log-filtering) diff --git a/apps/docs/content/guides/telemetry.mdx b/apps/docs/content/guides/telemetry.mdx deleted file mode 100644 index 8fb40504b9f..00000000000 --- a/apps/docs/content/guides/telemetry.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Telemetry ---- - -Telemetry helps you understand what’s happening inside your app by collecting logs, metrics, and traces. - -- **Logs** capture individual events, such as errors or warnings, providing details about what happened at a specific moment. -- **Metrics** track numerical data over time, like request latency or database query performance, helping you spot trends. -- **Traces** show the flow of a request through different services, helping you debug slow or failing operations. - -Supabase is working towards full support for the [OpenTelemetry](https://opentelemetry.io/) standard, making it easier to integrate with observability tools. - -This section provides guidance on telemetry in Supabase, including how to work with Supabase Logs. diff --git a/apps/docs/content/troubleshooting/exhaust-disk-io.mdx b/apps/docs/content/troubleshooting/exhaust-disk-io.mdx index 555860671bb..0df86d3eaac 100644 --- a/apps/docs/content/troubleshooting/exhaust-disk-io.mdx +++ b/apps/docs/content/troubleshooting/exhaust-disk-io.mdx @@ -25,7 +25,7 @@ Running out of Disk IO Budget means that your instance is using more disk than i To check your Disk IO Budget on the Supabase Platform, head over to [Database Health in the Observability section](/dashboard/project/_/observability/database). -It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to pinpoint potential causes and see more fine-grained metrics like how much of your RAM is used for caching and your Swap usage. Read the [Metrics Guide](/docs/guides/telemetry/metrics) to learn more. +It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to pinpoint potential causes and see more fine-grained metrics like how much of your RAM is used for caching and your Swap usage. Read the [Metrics Guide](/docs/guides/monitoring-and-debugging/metrics) to learn more. ## Common reasons for high disk IO usage diff --git a/apps/docs/content/troubleshooting/exhaust-ram.mdx b/apps/docs/content/troubleshooting/exhaust-ram.mdx index 51601f7c256..30351258315 100644 --- a/apps/docs/content/troubleshooting/exhaust-ram.mdx +++ b/apps/docs/content/troubleshooting/exhaust-ram.mdx @@ -31,7 +31,7 @@ High RAM usage could come with a range of issues: To check your RAM usage on the Supabase Platform, head over to [Database Health in the Observability section](/dashboard/project/_/observability/database). -It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to see how much of your RAM is used for caching and you can track other metrics such as your Swap usage. Read the [Metrics Guide](/docs/guides/telemetry/metrics) to learn more. +It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. With Grafana you will be able to see how much of your RAM is used for caching and you can track other metrics such as your Swap usage. Read the [Metrics Guide](/docs/guides/monitoring-and-debugging/metrics) to learn more. ## Common reasons for high RAM usage diff --git a/apps/docs/content/troubleshooting/exhaust-swap.mdx b/apps/docs/content/troubleshooting/exhaust-swap.mdx index f7c0bcf69c5..5f8b5c60f20 100644 --- a/apps/docs/content/troubleshooting/exhaust-swap.mdx +++ b/apps/docs/content/troubleshooting/exhaust-swap.mdx @@ -33,7 +33,7 @@ High Swap usage can affect your database performance. For example, you might see ## Monitor your swap -You can monitor your resources and set up alerts using Prometheus/Grafana. See the [metrics guide](/docs/guides/telemetry/metrics) for more information. +You can monitor your resources and set up alerts using Prometheus/Grafana. See the [metrics guide](/docs/guides/monitoring-and-debugging/metrics) for more information. An [example repository](https://github.com/supabase/supabase-grafana) to ingest metrics and visualize them with Grafana is provided in the linked guide, where we maintain a [list of the exported metrics](https://github.com/supabase/supabase-grafana/blob/main/docs/metrics.md). diff --git a/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx b/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx index 5160ea8cbb5..acda37f4d4c 100644 --- a/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx +++ b/apps/docs/content/troubleshooting/failed-to-retrieve-tables.mdx @@ -43,4 +43,4 @@ Once you are confident there will not be a crash loop, you can review the follow - Continue to monitor your project's [query performance tab](/dashboard/project/_/observability/query-performance) and [enable index advisor](/docs/guides/database/extensions/index_advisor) if you haven't already - especially if there are a lot of select queries. - If after monitoring your changes you still do not notice improvements, consider upgrading compute if you think this level of activity is going to be regular. It will give you more memory overhead to process tasks like this. You can view all compute offerings [here](/dashboard/project/_/settings/infrastructure). -If you want to effectively monitor your project's performance minute by minute, you can use the [Metrics API](/docs/guides/telemetry/metrics). +If you want to effectively monitor your project's performance minute by minute, you can use the [Metrics API](/docs/guides/monitoring-and-debugging/metrics). diff --git a/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx b/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx index 83e85220dc7..351692dc37c 100644 --- a/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx +++ b/apps/docs/content/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.mdx @@ -23,4 +23,4 @@ Review the appropriate guides based on your scenario: - [High Disk I/O](/docs/guides/troubleshooting/exhaust-disk-io) - [Query optimization](/docs/guides/database/query-optimization) -You can also set up alerts using a [Prometheus endpoint / Grafana charts](/docs/guides/telemetry/metrics) to monitor vital resources. +You can also set up alerts using a [Prometheus endpoint / Grafana charts](/docs/guides/monitoring-and-debugging/metrics) to monitor vital resources. diff --git a/apps/docs/content/troubleshooting/high-cpu-usage.mdx b/apps/docs/content/troubleshooting/high-cpu-usage.mdx index b80d3dc4e61..eedcf49ba23 100644 --- a/apps/docs/content/troubleshooting/high-cpu-usage.mdx +++ b/apps/docs/content/troubleshooting/high-cpu-usage.mdx @@ -25,7 +25,7 @@ You can check your CPU usage directly on the Supabase Platform. For this go to d ![CPU usage reported on Supabase dashboard](/docs/img/guides/platform/exhaust-cpu-report.png) -It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. You can find a guide for this [here](/docs/guides/telemetry/metrics). +It is also possible to monitor your resources and set up alerts using Prometheus/Grafana. You can find a guide for this [here](/docs/guides/monitoring-and-debugging/metrics). ## Common reasons for high CPU usage diff --git a/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx b/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx index c0ece19c525..a05664e2654 100644 --- a/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx +++ b/apps/docs/content/troubleshooting/how-to-interpret-and-explore-the-postgres-logs-OuCIOj.mdx @@ -398,5 +398,5 @@ To see the default types of events that are logged, you can check this [guide](h - [Debugging with the DB API logs](https://github.com/orgs/supabase/discussions/22849) - [Debugging Database Functions](/docs/guides/database/functions#debugging-functions) - [pg_audit](/docs/guides/database/extensions/pgaudit) -- [Supabase Logging](/docs/guides/telemetry/logs) +- [Supabase Logging](/docs/guides/monitoring-and-debugging/logs) - [Self-Hosting Logs](/docs/reference/self-hosting-analytics/introduction) diff --git a/apps/docs/content/troubleshooting/how-to-view-database-metrics-uqf2z_.mdx b/apps/docs/content/troubleshooting/how-to-view-database-metrics-uqf2z_.mdx index e5a1b47614d..ee301203a7d 100644 --- a/apps/docs/content/troubleshooting/how-to-view-database-metrics-uqf2z_.mdx +++ b/apps/docs/content/troubleshooting/how-to-view-database-metrics-uqf2z_.mdx @@ -7,6 +7,6 @@ keywords = [ "metrics", "grafana", "monitoring" ] database_id = "da2d95e5-abc5-47c8-8389-1554d12abf91" --- -To monitor real-time metrics of your database, like CPU, EBS, active database connections, and memory usage, you can deploy a Grafana Dashboard. Check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local or free [Fly.io](http://fly.io/) deployments. Refer to our concise [documentation](/docs/guides/telemetry/metrics) to learn more about the metrics endpoint. +To monitor real-time metrics of your database, like CPU, EBS, active database connections, and memory usage, you can deploy a Grafana Dashboard. Check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local or free [Fly.io](http://fly.io/) deployments. Refer to our concise [documentation](/docs/guides/monitoring-and-debugging/metrics) to learn more about the metrics endpoint. While the [Dashboard's Reports Page](/dashboard/project/_/observability) displays some metric data, it provides hourly averages, not real-time by the second data. However, it offers query metrics, which the Grafana Dashboard does not include. diff --git a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx index c84fc4b9776..430f9985367 100644 --- a/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx +++ b/apps/docs/content/troubleshooting/interpreting-supabase-grafana-io-charts-MUynDR.mdx @@ -7,7 +7,7 @@ keywords = [ "io", "disk", "database", "grafana" ] database_id = "0056cd40-df04-4045-bbfb-c245cb15b85d" --- -> [Supabase Grafana Installation Guide](/docs/guides/telemetry/metrics/grafana-self-hosted) +> [Supabase Grafana Installation Guide](/docs/guides/monitoring-and-debugging/metrics/grafana-self-hosted) There are two primary values that matter for IO: diff --git a/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx b/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx index 8db9630f2d0..6b1592a592a 100644 --- a/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx +++ b/apps/docs/content/troubleshooting/monitor-supavisor-postgres-connections.mdx @@ -19,7 +19,7 @@ _Visual of Grafana Dashboard_ It can be run locally within Docker. Alternatively, you can deploy it to fly.io or Grafana Cloud, which are better for long-term data collection. -Installation instructions can be found in it the [metrics docs ](/docs/guides/telemetry/metrics/grafana-self-hosted) +Installation instructions can be found in it the [metrics docs ](/docs/guides/monitoring-and-debugging/metrics/grafana-self-hosted) ## Observing connections diff --git a/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx b/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx index 0409f56edb8..2abdb07268c 100644 --- a/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx +++ b/apps/docs/content/troubleshooting/steps-to-improve-query-performance-with-indexes-q8PoC9.mdx @@ -23,7 +23,7 @@ Supabase has an [open-source Grafana Repo](https://github.com/supabase/supabase- _Visual of Grafana Dashboard_ ![image](/docs/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png) -It can be run locally within Docker or can be deployed for free to fly.io. Installation instructions can be found in [Supabase's metrics docs](/docs/guides/telemetry/metrics/grafana-self-hosted) +It can be run locally within Docker or can be deployed for free to fly.io. Installation instructions can be found in [Supabase's metrics docs](/docs/guides/monitoring-and-debugging/metrics/grafana-self-hosted) ### Query optimization through indexes diff --git a/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx b/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx index ffe0ad78aec..de577915a8d 100644 --- a/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx +++ b/apps/docs/content/troubleshooting/supabase-grafana-memory-charts.mdx @@ -7,7 +7,7 @@ date_created = "2024-06-05" database_id = "179d70f3-1e26-4346-9ee8-d340fad382a3" --- -> [Supabase Grafana Installation Guide](/docs/guides/telemetry/metrics/grafana-self-hosted) +> [Supabase Grafana Installation Guide](/docs/guides/monitoring-and-debugging/metrics/grafana-self-hosted) Here are examples of unhealthy memory usage: ![image](https://github.com/supabase/supabase/assets/91111415/baebfc74-642d-4988-992c-bb0f473a05ad) diff --git a/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx index 0bf72e502c8..1b1ed37893b 100644 --- a/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx +++ b/apps/docs/content/troubleshooting/supavisor-faq-YyP5tI.mdx @@ -159,7 +159,7 @@ As a rule of thumb, if you're using the DB REST API or multiple app-based "user+ Connection usage can be monitored with a Supabase Grafana Dashboard. It provides realtime visibility of over 200 database metrics, such as graphs of CPU, EBS, and active direct/pooler connections. It can be extremely useful for monitoring and debugging instances. -You can check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local deployments or free cloud deployments on [Fly.io](http://fly.io/). Refer to Supabase [documentation](/docs/guides/telemetry/metrics) to learn more about the metrics endpoint. +You can check our [GitHub repo](https://github.com/supabase/supabase-grafana) for setup instructions for local deployments or free cloud deployments on [Fly.io](http://fly.io/). Refer to Supabase [documentation](/docs/guides/monitoring-and-debugging/metrics) to learn more about the metrics endpoint. ### **Can Supavisor really support a million connections?** diff --git a/apps/docs/data/content-listings/index.ts b/apps/docs/data/content-listings/index.ts index c4320456003..bf0e5f93537 100644 --- a/apps/docs/data/content-listings/index.ts +++ b/apps/docs/data/content-listings/index.ts @@ -21,6 +21,7 @@ import { selfHostingShareExperience, } from './self-hosting.data' import { storageExamples, storageGetStarted, storageResources } from './storage.data' +import { telemetryDebugging, telemetryMonitoring } from './telemetry.data' const ALL_GROUPS: readonly ContentListingGroup[] = [ aiToolsSupportedAgents, @@ -48,6 +49,8 @@ const ALL_GROUPS: readonly ContentListingGroup[] = [ storageGetStarted, storageExamples, storageResources, + telemetryDebugging, + telemetryMonitoring, ] export const CONTENT_LISTINGS: Readonly> = Object.fromEntries( diff --git a/apps/docs/data/content-listings/log-drains.data.ts b/apps/docs/data/content-listings/log-drains.data.ts index d828ff2e923..d8f4b9f7f0c 100644 --- a/apps/docs/data/content-listings/log-drains.data.ts +++ b/apps/docs/data/content-listings/log-drains.data.ts @@ -9,55 +9,55 @@ export const logDrainsDestinations: ContentListingGroup = { { title: 'Custom Endpoint', description: 'Forward logs as a POST request to any custom HTTP endpoint.', - href: '/guides/telemetry/log-drains#custom-endpoint', + href: '/guides/monitoring-and-debugging/log-drains#custom-endpoint', icon: { kind: 'braces', color: '#3ECF8E', bg: 'rgba(62,207,142,0.1)' }, }, { title: 'OpenTelemetry (OTLP)', description: 'Send logs to any OTLP-compatible endpoint using Protocol Buffers over HTTP.', - href: '/guides/telemetry/log-drains#opentelemetry-otlp', + href: '/guides/monitoring-and-debugging/log-drains#opentelemetry-otlp', icon: { kind: 'otlp', color: '#F5A623', bg: 'rgba(245,166,35,0.1)' }, }, { title: 'Datadog', description: 'Stream logs directly into Datadog for monitoring and analysis.', - href: '/guides/telemetry/log-drains#datadog', + href: '/guides/monitoring-and-debugging/log-drains#datadog', icon: { kind: 'datadog', color: '#632CA6', bg: 'rgba(99,44,166,0.1)' }, }, { title: 'Loki', description: 'Ingest logs into Grafana Loki using the HTTP push API.', - href: '/guides/telemetry/log-drains#loki', + href: '/guides/monitoring-and-debugging/log-drains#loki', icon: { kind: 'grafana', color: '#F05A28', bg: 'rgba(240,90,40,0.1)' }, }, { title: 'Amazon S3', description: 'Write batched log files directly to an S3 bucket you own.', - href: '/guides/telemetry/log-drains#amazon-s3', + href: '/guides/monitoring-and-debugging/log-drains#amazon-s3', icon: { kind: 'cloud', color: '#FF9900', bg: 'rgba(255,153,0,0.1)' }, }, { title: 'Sentry', description: "Send logs to Sentry's Logging product for filtering and grouping.", - href: '/guides/telemetry/log-drains#sentry', + href: '/guides/monitoring-and-debugging/log-drains#sentry', icon: { kind: 'sentry', color: '#362D59', bg: 'rgba(54,45,89,0.1)' }, }, { title: 'Axiom', description: 'Forward logs to an Axiom dataset for storage and analysis.', - href: '/guides/telemetry/log-drains#axiom', + href: '/guides/monitoring-and-debugging/log-drains#axiom', icon: { kind: 'axiom', color: '#6366F1', bg: 'rgba(99,102,241,0.1)' }, }, { title: 'Last9', description: 'Stream logs to Last9 for OpenTelemetry-native observability.', - href: '/guides/telemetry/log-drains#last9', + href: '/guides/monitoring-and-debugging/log-drains#last9', icon: { kind: 'last9', color: '#00B4A0', bg: 'rgba(0,180,160,0.1)' }, }, { title: 'Syslog', description: 'Forward logs to a remote Syslog receiver over TCP or TLS (RFC 5424).', - href: '/guides/telemetry/log-drains#syslog', + href: '/guides/monitoring-and-debugging/log-drains#syslog', icon: { kind: 'server', color: '#64748B', bg: 'rgba(100,116,139,0.1)' }, }, ], diff --git a/apps/docs/data/content-listings/telemetry.data.ts b/apps/docs/data/content-listings/telemetry.data.ts new file mode 100644 index 00000000000..93ba564a300 --- /dev/null +++ b/apps/docs/data/content-listings/telemetry.data.ts @@ -0,0 +1,65 @@ +import type { ContentListingGroup } from '~/lib/content-listings.schema' + +export const telemetryDebugging: ContentListingGroup = { + id: 'telemetry-debugging', + heading: 'Debugging', + type: 'grid', + columns: 2, + items: [ + { + title: 'Debugging guide', + href: '/guides/monitoring-and-debugging/debugging', + description: + 'Isolate the failing layer, read logs as evidence, and match symptoms to troubleshooting guides.', + }, + { + title: 'Logging', + href: '/guides/monitoring-and-debugging/logs', + description: 'Query events from any Supabase service using the Logs Explorer.', + }, + { + title: 'Advanced log filtering', + href: '/guides/monitoring-and-debugging/advanced-log-filtering', + description: 'Regex filtering, structured-field queries, and field discovery in ClickHouse.', + }, + { + title: 'Troubleshooting index', + href: '/guides/troubleshooting', + description: 'Searchable index of known error codes, symptoms, and fixes.', + }, + ], +} + +export const telemetryMonitoring: ContentListingGroup = { + id: 'telemetry-monitoring', + heading: 'Monitoring', + type: 'grid', + columns: 2, + items: [ + { + title: 'Log drains', + href: '/guides/monitoring-and-debugging/log-drains', + description: 'Forward logs to Datadog, Loki, Axiom, S3, or a custom HTTP endpoint.', + }, + { + title: 'Reports', + href: '/guides/monitoring-and-debugging/reports', + description: 'Built-in dashboards for API, Auth, Storage, and Realtime activity.', + }, + { + title: 'Metrics', + href: '/guides/monitoring-and-debugging/metrics', + description: 'Prometheus-compatible database metrics for Grafana and other tools.', + }, + { + title: 'Client-side tracing', + href: '/guides/monitoring-and-debugging/client-side-tracing', + description: 'Correlate browser requests end-to-end using W3C Trace Context.', + }, + { + title: 'Sentry integration', + href: '/guides/monitoring-and-debugging/sentry-monitoring', + description: 'Send errors to Sentry for alerting and grouping.', + }, + ], +} diff --git a/apps/docs/features/docs/GuidesMdx.utils.tsx b/apps/docs/features/docs/GuidesMdx.utils.tsx index fc47f2664ae..70a23ed0961 100644 --- a/apps/docs/features/docs/GuidesMdx.utils.tsx +++ b/apps/docs/features/docs/GuidesMdx.utils.tsx @@ -33,6 +33,7 @@ const PUBLISHED_SECTIONS = [ 'graphql', 'integrations', 'local-development', + 'monitoring-and-debugging', 'platform', 'queues', 'realtime', @@ -40,7 +41,6 @@ const PUBLISHED_SECTIONS = [ 'security', 'self-hosting', 'storage', - 'telemetry', ] as const const getGuidesMarkdownInternal = async (slug: string[]) => { diff --git a/apps/docs/lib/breadcrumbs.ts b/apps/docs/lib/breadcrumbs.ts index 6ca51d642bc..ac6643c80b5 100644 --- a/apps/docs/lib/breadcrumbs.ts +++ b/apps/docs/lib/breadcrumbs.ts @@ -26,7 +26,7 @@ const SECTION_PATH_TO_KEY: Record = { security: 'security', 'self-hosting': 'self_hosting', storage: 'storage', - telemetry: 'telemetry', + 'monitoring-and-debugging': 'telemetry', } function getSectionMenu(pathname: string) { diff --git a/apps/docs/lib/content-listings.test.ts b/apps/docs/lib/content-listings.test.ts index 37d54128d73..0c609a60699 100644 --- a/apps/docs/lib/content-listings.test.ts +++ b/apps/docs/lib/content-listings.test.ts @@ -189,7 +189,7 @@ describe('dashboard content listing hrefs', () => { describe('contentListingItemSchema icon', () => { const baseItem = { title: 'Datadog', - href: '/guides/telemetry/log-drains#datadog', + href: '/guides/monitoring-and-debugging/log-drains#datadog', description: 'Stream logs directly into Datadog for monitoring and analysis.', } diff --git a/apps/www/lib/redirects.js b/apps/www/lib/redirects.js index ccff21267ae..33e95d0c4dd 100644 --- a/apps/www/lib/redirects.js +++ b/apps/www/lib/redirects.js @@ -119,6 +119,11 @@ module.exports = [ source: '/docs/guides/reports/:match*', destination: '/docs/guides/observability/:match*', }, + { + permanent: true, + source: '/docs/guides/telemetry/:match*', + destination: '/docs/guides/monitoring-and-debugging/:match*', + }, { permanent: false, source: '/blog/2021/03/08/toad-a-link-shorterner-with-simple-apis-for-low-coders', From 5a3e3598d0a383830f7eccd1acf53035081af02c Mon Sep 17 00:00:00 2001 From: Daniel Guerra <15204776+danielmx-dev@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:55:40 -0600 Subject: [PATCH 126/141] feat(billing): Lazy load BillingSettings queries for components that are not in view (#48454) ## What kind of change does this PR introduce? There are some expensive queries that are requested every time the Billing Settings page loads, some of them happen for components that are outside of the initial viewport. We can avoid performing those requests unless the user explicitly scrolls to the relevant section. A similar pattern has been implemented in the past for the Credit Balance section in https://github.com/supabase/supabase/pull/45481. I decided to skip the lazy load pattern in the components that appear at the top of the Billing Settings (Subscription and Cost Control). ## What is the current behavior? When loading the billing settings page, all components (except CreditBalance) will perform requests to load the data they need. ## What is the new behavior? The components will perform the request to load their data until they are inside the viewport. You can verify this by opening the developer tools and monitor the requests as you scroll through the page. ## Testing These changes can be easily tested by making your browser's height very short then opening the org billing settings `/org/{slug}/billing`. Open the developer tools and filter for XHR requests. As you scroll through the page, the requests will be made. To verify that all potential requests are covered, I compared the requests done during a fresh load with another settings page, the only extra requests made by the billing settings page are the ones relevant to the components at the very top (or the BillingSettings container itself, which requests the subscription). ## Summary by CodeRabbit * **Performance Improvements** * Billing, payment methods, billing customer details, upcoming invoices, and invoice lists now load only when their sections scroll into view, reducing initial loading. * Existing access/permission checks are still applied before fetching billing-related data. * **Tests** * Updated billing and invoices tests to mock viewport intersection behavior (IntersectionObserver) to match the new lazy-loading behavior. --- .../Payment/PaymentMethods/PaymentMethods.test.tsx | 3 +++ .../Payment/PaymentMethods/PaymentMethods.tsx | 8 +++++--- .../BillingBreakdown/UpcomingInvoice.tsx | 9 ++++++--- .../BillingCustomerData/BillingCustomerData.test.tsx | 3 +++ .../BillingCustomerData/BillingCustomerData.tsx | 12 +++++++++--- .../Organization/BillingSettings/BillingEmail.tsx | 7 +++++-- .../InvoicesSettings/InvoicesSettings.test.tsx | 3 +++ .../InvoicesSettings/InvoicesSettings.tsx | 10 +++++++--- 8 files changed, 41 insertions(+), 14 deletions(-) diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.test.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.test.tsx index fecbfd6e02f..29dbe25a73c 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.test.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.test.tsx @@ -1,10 +1,13 @@ import { screen } from '@testing-library/react' +import { mockIntersectionObserver } from 'jsdom-testing-mocks' import { beforeEach, describe, expect, it, vi } from 'vitest' import PaymentMethods from './PaymentMethods' import { MANAGED_BY } from '@/lib/constants/infrastructure' import { createMockOrganization, render } from '@/tests/helpers' +mockIntersectionObserver() + const { mockSelectedOrganization, mockPaymentMethodsQuery, mockSubscription } = vi.hoisted(() => ({ mockSelectedOrganization: vi.fn(), mockPaymentMethodsQuery: vi.fn(), diff --git a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx index fda065cad11..09ef0267878 100644 --- a/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx +++ b/apps/studio/components/interfaces/Billing/Payment/PaymentMethods/PaymentMethods.tsx @@ -2,6 +2,7 @@ import { PermissionAction, SupportCategories } from '@supabase/shared-types/out/ import { useParams } from 'common' import { CreditCardIcon, ExternalLink, Plus } from 'lucide-react' import { useState } from 'react' +import { useInView } from 'react-intersection-observer' import { toast } from 'sonner' import { Button } from 'ui' import { Admonition } from 'ui-patterns/Admonition' @@ -37,14 +38,15 @@ const PaymentMethods = () => { const [selectedMethodToDelete, setSelectedMethodToDelete] = useState() const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = useState(false) - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: slug }) + const { ref, inView } = useInView({ triggerOnce: true }) + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: inView }) const { data: paymentMethods, error, isPending: isLoading, isError, isSuccess, - } = useOrganizationPaymentMethodsQuery({ slug }) + } = useOrganizationPaymentMethodsQuery({ slug }, { enabled: inView }) const { can: canReadPaymentMethods, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.BILLING_READ, @@ -61,7 +63,7 @@ const PaymentMethods = () => { selectedOrganization?.managed_by === MANAGED_BY.STRIPE_PROJECTS return ( <> - +

    Payment Methods

    diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx index 3b8c7f2390e..eddb54f4696 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/UpcomingInvoice.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { useInView } from 'react-intersection-observer' import { Table, TableBody, TableCell, TableFooter, TableRow } from 'ui' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' @@ -53,13 +54,15 @@ const usageBillingDocsLink: { [K in PricingMetric]?: string } = { } export const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { + const { ref, inView } = useInView({ triggerOnce: true }) + const { data: upcomingInvoice, error: error, isPending: isLoading, isError, isSuccess, - } = useOrgUpcomingInvoiceQuery({ orgSlug: slug }) + } = useOrgUpcomingInvoiceQuery({ orgSlug: slug }, { enabled: inView }) const { data: organization } = useOrganizationQuery({ slug }) @@ -109,7 +112,7 @@ export const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { !planItem && upcomingInvoice?.fixed_fees_billing_mode === 'in_arrears' return ( - <> +
    {isLoading && (
    @@ -398,7 +401,7 @@ export const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => {
    )} - +
    ) } diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.test.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.test.tsx index e2ae0ef10ab..203c3879cc1 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.test.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.test.tsx @@ -1,4 +1,5 @@ import { screen } from '@testing-library/react' +import { mockIntersectionObserver } from 'jsdom-testing-mocks' import type { ReactNode } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -6,6 +7,8 @@ import { BillingCustomerData } from './BillingCustomerData' import { MANAGED_BY } from '@/lib/constants/infrastructure' import { createMockOrganization, render } from '@/tests/helpers' +mockIntersectionObserver() + const mockSelectedOrganization = vi.hoisted(() => vi.fn()) vi.mock('common', async (importOriginal) => { diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx index 98c584f1744..42f0369936f 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { useTheme } from 'next-themes' import { useEffect, useMemo, useRef, useState } from 'react' +import { useInView } from 'react-intersection-observer' import { toast } from 'sonner' import { Button, Card, CardFooter, Form } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' @@ -47,19 +48,24 @@ export const BillingCustomerData = () => { 'stripe.customer' ) + const { ref, inView } = useInView({ triggerOnce: true }) + const { data: customerProfile, error, isPending: isLoading, isSuccess, - } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingCustomerData }) + } = useOrganizationCustomerProfileQuery( + { slug }, + { enabled: canReadBillingCustomerData && inView } + ) const { data: taxId, error: errorLoadingTaxId, isPending: isLoadingTaxId, isSuccess: loadedTaxId, - } = useOrganizationTaxIdQuery({ slug }) + } = useOrganizationTaxIdQuery({ slug }, { enabled: inView }) const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation({ onError: () => {}, @@ -165,7 +171,7 @@ export const BillingCustomerData = () => { ) return ( - +

    Billing Address & Tax ID

    diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx index 6e3a172b0d4..f2b7e943f76 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx @@ -4,6 +4,7 @@ import { Form, FormControl, FormField } from '@ui/components/shadcn/ui/form' import { useParams } from 'common' import { useEffect } from 'react' import { useForm, useWatch } from 'react-hook-form' +import { useInView } from 'react-intersection-observer' import { toast } from 'sonner' import { FormMessage, Input } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -51,8 +52,10 @@ const BillingEmail = () => { 'organizations' ) + const { ref, inView } = useInView({ triggerOnce: true }) + const { data: billingCustomer, isPending: loadingBillingCustomer } = - useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingEmail }) + useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingEmail && inView }) const form = useForm>({ resolver: zodResolver(formSchema), @@ -103,7 +106,7 @@ const BillingEmail = () => { }, [billingCustomer]) return ( - +

    Email Recipient

    diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.test.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.test.tsx index 2c9d8d58344..baffd645a80 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.test.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.test.tsx @@ -1,10 +1,13 @@ import { screen } from '@testing-library/react' +import { mockIntersectionObserver } from 'jsdom-testing-mocks' import { beforeEach, describe, expect, it, vi } from 'vitest' import { InvoicesSettings } from './InvoicesSettings' import { MANAGED_BY } from '@/lib/constants/infrastructure' import { createMockOrganization, render } from '@/tests/helpers' +mockIntersectionObserver() + const { mockSelectedOrganization, mockInvoicesQuery, mockInvoicesCountQuery } = vi.hoisted(() => ({ mockSelectedOrganization: vi.fn(), mockInvoicesQuery: vi.fn(), diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx index b25978116dd..ebbb793c58d 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSettings.tsx @@ -1,6 +1,7 @@ import dayjs from 'dayjs' import { ChevronLeft, ChevronRight, FileText, Receipt, ScrollText } from 'lucide-react' import { useEffect, useState } from 'react' +import { useInView } from 'react-intersection-observer' import { toast } from 'sonner' import { Button, @@ -56,13 +57,16 @@ export const InvoicesSettings = () => { const isPartnerBilledOrganization = isPartnerBillingOrganization( selectedOrganization?.billing_partner ) + + const { ref, inView } = useInView({ triggerOnce: true }) + const offset = (page - 1) * PAGE_LIMIT const { data: count, isError: isErrorCount } = useInvoicesCountQuery( { slug, }, - { enabled: !isPartnerBilledOrganization } + { enabled: !isPartnerBilledOrganization && inView } ) const { data, @@ -75,7 +79,7 @@ export const InvoicesSettings = () => { offset, limit: PAGE_LIMIT, }, - { enabled: !isPartnerBilledOrganization } + { enabled: !isPartnerBilledOrganization && inView } ) const invoices = data || [] @@ -118,7 +122,7 @@ export const InvoicesSettings = () => { isLoading || invoices.length === 0 ? 'text-foreground-muted' : undefined return ( - + From 3c305148186c311db7ec03f2e17e91ff94de1200 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:15:52 +0300 Subject: [PATCH 127/141] =?UTF-8?q?fix:=20open=20integrated=20support=20fo?= =?UTF-8?q?rm=20from=20Feedback=20=E2=86=92=20Issue=20=E2=86=92=20Contact?= =?UTF-8?q?=20sup=E2=80=A6=20(#48488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR description: ## Summary - Clicking Feedback → Issue → Contact support was navigating to `/support/new` (the old full-page form) instead of opening the integrated sidebar support form - Fixed by setting a `helpPanelState.requestedView` signal before opening the Help sidebar, so it opens directly at the support form view - Added a small valtio store (`state/help-panel-state.ts`) to communicate the desired view between `FeedbackDropdown` and `HelpPanel` ## Test plan - [ ] Feedback → Issue → Contact support opens the Help sidebar at the support form (not `/support/new`) - [ ] Help button → Contact support still works as before - [ ] Closing and reopening the Help sidebar via the Help button opens at the home view ## Summary by CodeRabbit * **New Features** * Selecting **Support** from the Help menu now opens the Help Panel directly to the Support view. * The Help Panel automatically updates to the requested section when opened. --- .../FeedbackDropdown/FeedbackDropdown.tsx | 8 +++++++- apps/studio/components/ui/HelpPanel/HelpPanel.tsx | 15 +++++++++++++-- apps/studio/state/help-panel-state.ts | 7 +++++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 apps/studio/state/help-panel-state.ts diff --git a/apps/studio/components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx b/apps/studio/components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx index f0df629c90c..96f4df3b314 100644 --- a/apps/studio/components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx +++ b/apps/studio/components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackDropdown.tsx @@ -13,6 +13,7 @@ import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganizati import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useTrack } from '@/lib/telemetry/track' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' +import { helpPanelState } from '@/state/help-panel-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' export const FeedbackDropdown = ({ className }: { className?: string }) => { @@ -98,7 +99,12 @@ export const FeedbackDropdown = ({ className }: { className?: string }) => { snap.newChat(ASSISTANT_SUGGESTIONS) setIsOpen(false) }} - onSupportClick={() => setIsOpen(false)} + onSupportClick={() => { + helpPanelState.requestedView = 'support' + setIsOpen(false) + openSidebar(SIDEBAR_KEYS.HELP_PANEL) + return false + }} /> diff --git a/apps/studio/components/ui/HelpPanel/HelpPanel.tsx b/apps/studio/components/ui/HelpPanel/HelpPanel.tsx index 211639e3e9b..c5f0a90da1b 100644 --- a/apps/studio/components/ui/HelpPanel/HelpPanel.tsx +++ b/apps/studio/components/ui/HelpPanel/HelpPanel.tsx @@ -2,9 +2,10 @@ import { IS_PLATFORM } from 'common' import { ChevronLeft, X } from 'lucide-react' import Image from 'next/image' import { useRouter } from 'next/router' -import { useState } from 'react' +import { useEffect, useState } from 'react' import SVG from 'react-inlinesvg' import { Button } from 'ui' +import { useSnapshot } from 'valtio' import { ASSISTANT_SUGGESTIONS } from './HelpPanel.constants' import { HelpSection } from './HelpSection' @@ -16,6 +17,7 @@ import { import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' +import { helpPanelState } from '@/state/help-panel-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' export const HelpPanel = ({ @@ -30,7 +32,16 @@ export const HelpPanel = ({ const snap = useAiAssistantStateSnapshot() const { openSidebar, closeSidebar } = useSidebarManagerSnapshot() const router = useRouter() - const [view, setView] = useState<'home' | 'support'>('home') + const helpSnap = useSnapshot(helpPanelState) + const [view, setView] = useState<'home' | 'support'>(() => helpPanelState.requestedView) + + useEffect(() => { + if (helpSnap.requestedView !== 'home') { + setView(helpSnap.requestedView) + helpPanelState.requestedView = 'home' + } + }, [helpSnap.requestedView]) + const isSupportView = view === 'support' const openAssistant = () => { onClose() diff --git a/apps/studio/state/help-panel-state.ts b/apps/studio/state/help-panel-state.ts new file mode 100644 index 00000000000..100959fa74b --- /dev/null +++ b/apps/studio/state/help-panel-state.ts @@ -0,0 +1,7 @@ +import { proxy } from 'valtio' + +export const helpPanelState = proxy<{ + requestedView: 'home' | 'support' +}>({ + requestedView: 'home', +}) From e5587799692aec7e59849e9e39f0536b1e93fd0d Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:50:06 +0200 Subject: [PATCH 128/141] Fix: RadioGroupCard focus state design (#48494) Fix the `` focus state design Before: image After: image ## Summary by CodeRabbit * **Style** * Updated radio group card selection styling for a cleaner, more consistent focus and checked-state appearance. * Added improved outline handling for radio group card items. --- packages/ui/src/components/radio-group-card.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/ui/src/components/radio-group-card.tsx b/packages/ui/src/components/radio-group-card.tsx index fc58fa7757c..2bf4330a345 100644 --- a/packages/ui/src/components/radio-group-card.tsx +++ b/packages/ui/src/components/radio-group-card.tsx @@ -38,10 +38,7 @@ const RadioGroupCardItem = React.forwardRef< // 'hover:bg-selection', 'hover:border-foreground-muted', 'hover:z-1 focus-visible:z-1', - 'data-[state=checked]:z-1', - 'data-[state=checked]:ring-2 data-[state=checked]:ring-border', - 'data-[state=checked]:bg-surface-300 dark:data-[state=checked]:bg-surface-300', - 'data-[state=checked]:border-foreground/50', + 'outline-hidden', 'transition-colors', 'group', props.className From 63a74d488f2617865ade2d6131fb2dd34c609148 Mon Sep 17 00:00:00 2001 From: Thomas <31189692+ecktoteckto@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:31:13 +0200 Subject: [PATCH 129/141] chore(billing): rework texts on aws marketplace onboarding page (#47735) --- .../AwsMarketplaceOnboarding.components.tsx | 30 ++++++++++++++++--- packages/api-types/types/platform.d.ts | 5 +++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.components.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.components.tsx index 6c679a14c1f..e701eb9efd5 100644 --- a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.components.tsx +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.components.tsx @@ -94,11 +94,16 @@ export function getContractIneligibilityDescription( ) { switch (reason) { case 'AWS_ACTIVATE_CREDITS_DEAL': - return 'No further action is required for this AWS Activate credits offer' + return 'This private offer grants you credits on the Supabase platform' case 'AGREEMENT_BASED_OFFER': return 'This private offer updated an existing AWS Marketplace subscription' - case 'NO_ACTIVE_CONTRACT_FOUND': + case 'NO_CONTRACT_FOUND': + case 'CONTRACT_IN_SETTLING_WINDOW': return 'AWS is still syncing this Marketplace subscription' + case 'CONTRACT_INACTIVE': + return '' + case 'CONTRACT_TERMINATED_EARLY': + return 'Subscription was terminated' default: return 'This AWS Marketplace subscription cannot be linked right now' } @@ -114,7 +119,7 @@ export function ContractIneligibilityNotice({ return ( ) @@ -126,7 +131,8 @@ export function ContractIneligibilityNotice({ description="Your existing Supabase organization remains linked to AWS Marketplace and your projects will continue to run as usual." /> ) - case 'NO_ACTIVE_CONTRACT_FOUND': + case 'NO_CONTRACT_FOUND': + case 'CONTRACT_IN_SETTLING_WINDOW': return ( ) + case 'CONTRACT_INACTIVE': + return ( + + ) + case 'CONTRACT_TERMINATED_EARLY': + return ( + + ) default: return } diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 8ddbcaaecf4..836d4c4de6d 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -5155,7 +5155,10 @@ export interface components { reasons: ( | 'AWS_ACTIVATE_CREDITS_DEAL' | 'AGREEMENT_BASED_OFFER' - | 'NO_ACTIVE_CONTRACT_FOUND' + | 'NO_CONTRACT_FOUND' + | 'CONTRACT_INACTIVE' + | 'CONTRACT_TERMINATED_EARLY' + | 'CONTRACT_IN_SETTLING_WINDOW' )[] } } From 3e02ce74b8131b59c34b4fee26d670745d6b7ed7 Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:09:02 +0200 Subject: [PATCH 130/141] fix(self-hosted): pin rustfs to beta.11 and fix s3 tests (#48500) --- docker/docker-compose.rustfs.yml | 2 +- docker/tests/test-s3-backend.sh | 47 ++++++++++++++++++++------------ docker/tests/test-s3.sh | 41 ++++++++++++++++++---------- 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/docker/docker-compose.rustfs.yml b/docker/docker-compose.rustfs.yml index 147da6748dc..65026567b10 100644 --- a/docker/docker-compose.rustfs.yml +++ b/docker/docker-compose.rustfs.yml @@ -1,7 +1,7 @@ services: rustfs: - image: rustfs/rustfs + image: rustfs/rustfs:1.0.0-beta.11 environment: RUSTFS_ACCESS_KEY: ${MINIO_ROOT_USER} RUSTFS_SECRET_KEY: ${MINIO_ROOT_PASSWORD} diff --git a/docker/tests/test-s3-backend.sh b/docker/tests/test-s3-backend.sh index 799994acd7d..c7b31a12c37 100644 --- a/docker/tests/test-s3-backend.sh +++ b/docker/tests/test-s3-backend.sh @@ -67,11 +67,24 @@ check() { fi } -# Wrapper for aws commands against the backend directly +# Wrapper for aws commands against the backend directly. +# +# Always exits 0: under `set -e` a failing aws call would kill the suite on the +# spot, so the check never records a FAIL and no summary is printed. The aws +# error is echoed to stderr so a FAIL stays explainable even where the caller +# discards stdout. s3() { - AWS_ACCESS_KEY_ID="$BACKEND_ACCESS_KEY" \ - AWS_SECRET_ACCESS_KEY="$BACKEND_SECRET_KEY" \ - aws "$@" --endpoint-url "$BACKEND_URL" --region "$REGION" 2>&1 + s3_out=$(AWS_ACCESS_KEY_ID="$BACKEND_ACCESS_KEY" \ + AWS_SECRET_ACCESS_KEY="$BACKEND_SECRET_KEY" \ + aws "$@" --endpoint-url "$BACKEND_URL" --region "$REGION" 2>&1) || + echo " aws $1 $2 failed: $(printf '%s' "$s3_out" | tail -n 1)" >&2 + printf '%s\n' "$s3_out" +} + +# Wrapper for jq that yields empty output instead of aborting the suite when +# the payload is not JSON — an S3 error document, say — and jq exits non-zero. +jq_r() { + jq -r "$@" 2>/dev/null || true } bucket_name="backend-test-$$" @@ -86,7 +99,7 @@ echo "" echo "--- Connectivity ---" list_output=$(s3 s3api list-buckets --output json) -list_ok=$(echo "$list_output" | jq -r 'if .Buckets then "true" else "false" end' 2>/dev/null) +list_ok=$(echo "$list_output" | jq_r 'if .Buckets then "true" else "false" end') check "Backend reachable (ListBuckets)" "true" "$list_ok" if [ "$list_ok" != "true" ]; then @@ -105,7 +118,7 @@ echo "" echo "--- Storage bucket ---" if [ -n "$GLOBAL_S3_BUCKET" ]; then storage_bucket_exists=$(s3 s3api list-buckets --output json | \ - jq -r --arg name "$GLOBAL_S3_BUCKET" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null) + jq_r --arg name "$GLOBAL_S3_BUCKET" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end') check "GLOBAL_S3_BUCKET ($GLOBAL_S3_BUCKET) exists" "true" "$storage_bucket_exists" else echo " SKIP: GLOBAL_S3_BUCKET not set" @@ -117,11 +130,11 @@ fi echo "" echo "--- CreateBucket ---" -s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null 2>&1 +s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null # Verify create succeeded create_found=$(s3 s3api list-buckets --output json | \ - jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null) + jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end') check "CreateBucket" "true" "$create_found" if [ "$create_found" != "true" ]; then @@ -133,7 +146,7 @@ fi # Verify in ListBuckets (separate call) bucket_found=$(s3 s3api list-buckets --output json | \ - jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null) + jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end') check "Bucket visible in ListBuckets" "true" "$bucket_found" # --------------------------------------------- @@ -156,7 +169,7 @@ echo "" echo "--- ListObjectsV2 ---" list_objects=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json) object_found=$(echo "$list_objects" | \ - jq -r '[.Contents[]? | .Key] | if any(. == "test-file.txt") then "true" else "false" end' 2>/dev/null) + jq_r '[.Contents[]? | .Key] | if any(. == "test-file.txt") then "true" else "false" end') check "Object found in ListObjectsV2" "true" "$object_found" # --------------------------------------------- @@ -166,7 +179,7 @@ check "Object found in ListObjectsV2" "true" "$object_found" echo "" echo "--- HeadObject ---" head_output=$(s3 s3api head-object --bucket "$bucket_name" --key "test-file.txt" --output json) -head_size=$(echo "$head_output" | jq -r '.ContentLength // 0' 2>/dev/null) +head_size=$(echo "$head_output" | jq_r '.ContentLength // 0') original_size=$(wc -c < "$tmpfile" | tr -d ' ') check "HeadObject returns correct size" "$original_size" "$head_size" rm -f "$tmpfile" @@ -207,7 +220,7 @@ echo "--- DeleteObject ---" s3 s3 rm "s3://$bucket_name/test-copy.txt" >/dev/null list_after_delete=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json) copy_gone=$(echo "$list_after_delete" | \ - jq -r '[.Contents[]? | .Key] | if any(. == "test-copy.txt") then "false" else "true" end' 2>/dev/null) + jq_r '[.Contents[]? | .Key] | if any(. == "test-copy.txt") then "false" else "true" end') check "Deleted object no longer listed" "true" "$copy_gone" # --------------------------------------------- @@ -224,7 +237,7 @@ large_ok=$(echo "$large_put" | grep -q "upload:" && echo "true" || echo "false") check "Multipart upload (7MB)" "true" "$large_ok" large_head=$(s3 s3api head-object --bucket "$bucket_name" --key "large-file.bin" --output json) -remote_size=$(echo "$large_head" | jq -r '.ContentLength // 0' 2>/dev/null) +remote_size=$(echo "$large_head" | jq_r '.ContentLength // 0') check "Multipart size matches ($large_size bytes)" "$large_size" "$remote_size" large_download=$(mktemp); cleanup_files="$cleanup_files $large_download" @@ -250,12 +263,12 @@ rm -f "$batch_file" delete_objects_output=$(s3 s3api delete-objects --bucket "$bucket_name" \ --delete '{"Objects":[{"Key":"batch-a.txt"},{"Key":"batch-b.txt"},{"Key":"batch-c.txt"}]}' \ --output json) -deleted_count=$(echo "$delete_objects_output" | jq -r '.Deleted | length' 2>/dev/null) +deleted_count=$(echo "$delete_objects_output" | jq_r '.Deleted | length') check "DeleteObjects removed 3 objects" "3" "$deleted_count" # Verify all gone batch_list=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --prefix "batch-" --output json) -batch_remaining=$(echo "$batch_list" | jq -r '[.Contents[]?] | length' 2>/dev/null) +batch_remaining=$(echo "$batch_list" | jq_r '[.Contents[]?] | length') check "Batch-deleted objects gone" "0" "$batch_remaining" # --------------------------------------------- @@ -270,7 +283,7 @@ s3 s3 cp "$presign_file" "s3://$bucket_name/presign-test.txt" >/dev/null rm -f "$presign_file" presigned_url=$(s3 s3 presign "s3://$bucket_name/presign-test.txt") -presign_body=$(curl -s "$presigned_url") +presign_body=$(curl -s "$presigned_url" || true) check "Presigned URL returns correct content" "presigned content test" "$presign_body" # --------------------------------------------- @@ -342,7 +355,7 @@ echo "--- Cleanup ---" s3 s3 rm "s3://$bucket_name/" --recursive >/dev/null s3 s3api delete-bucket --bucket "$bucket_name" >/dev/null bucket_gone=$(s3 s3api list-buckets --output json | \ - jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end' 2>/dev/null) + jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end') check "Test bucket deleted" "true" "$bucket_gone" # --------------------------------------------- diff --git a/docker/tests/test-s3.sh b/docker/tests/test-s3.sh index 939547dd1c7..9810afe27fb 100644 --- a/docker/tests/test-s3.sh +++ b/docker/tests/test-s3.sh @@ -65,11 +65,24 @@ check() { fi } -# Wrapper for aws s3/s3api commands with correct endpoint and credentials +# Wrapper for aws s3/s3api commands with correct endpoint and credentials. +# +# Always exits 0: under `set -e` a failing aws call would kill the suite on the +# spot, so the check never records a FAIL and no summary is printed. The aws +# error is echoed to stderr so a FAIL stays explainable even where the caller +# discards stdout. s3() { - AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY" \ - AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \ - aws "$@" --endpoint-url "$S3_ENDPOINT" --region "$REGION" 2>&1 + s3_out=$(AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY" \ + AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \ + aws "$@" --endpoint-url "$S3_ENDPOINT" --region "$REGION" 2>&1) || + echo " aws $1 $2 failed: $(printf '%s' "$s3_out" | tail -n 1)" >&2 + printf '%s\n' "$s3_out" +} + +# Wrapper for jq that yields empty output instead of aborting the suite when +# the payload is not JSON — an S3 error document, say — and jq exits non-zero. +jq_r() { + jq -r "$@" 2>/dev/null || true } bucket_name="s3-test-$$" @@ -84,7 +97,7 @@ echo "" echo "--- S3 ListBuckets ---" list_output=$(s3 s3api list-buckets --output json) -list_ok=$(echo "$list_output" | jq -r 'if .Buckets then "true" else "false" end' 2>/dev/null) +list_ok=$(echo "$list_output" | jq_r 'if .Buckets then "true" else "false" end') check "ListBuckets returns valid response" "true" "$list_ok" # --------------------------------------------- @@ -93,11 +106,11 @@ check "ListBuckets returns valid response" "true" "$list_ok" echo "" echo "--- S3 CreateBucket ---" -s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null 2>&1 +s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null # Verify create succeeded create_found=$(s3 s3api list-buckets --output json | \ - jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null) + jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end') check "CreateBucket" "true" "$create_found" if [ "$create_found" != "true" ]; then @@ -109,7 +122,7 @@ fi # Verify bucket appears in ListBuckets (separate call) s3_bucket_found=$(s3 s3api list-buckets --output json | \ - jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end' 2>/dev/null) + jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end') check "Bucket visible in ListBuckets" "true" "$s3_bucket_found" # --------------------------------------------- @@ -132,7 +145,7 @@ echo "" echo "--- S3 ListObjectsV2 ---" list_objects=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json) object_found=$(echo "$list_objects" | \ - jq -r '[.Contents[]? | .Key] | if any(. == "s3-uploaded.txt") then "true" else "false" end' 2>/dev/null) + jq_r '[.Contents[]? | .Key] | if any(. == "s3-uploaded.txt") then "true" else "false" end') check "Object found in ListObjectsV2" "true" "$object_found" # --------------------------------------------- @@ -142,7 +155,7 @@ check "Object found in ListObjectsV2" "true" "$object_found" echo "" echo "--- S3 HeadObject ---" head_output=$(s3 s3api head-object --bucket "$bucket_name" --key "s3-uploaded.txt" --output json) -head_size=$(echo "$head_output" | jq -r '.ContentLength // 0' 2>/dev/null) +head_size=$(echo "$head_output" | jq_r '.ContentLength // 0') original_size=$(wc -c < "$tmpfile" | tr -d ' ') check "HeadObject returns correct size" "$original_size" "$head_size" rm -f "$tmpfile" @@ -185,7 +198,7 @@ s3 s3 rm "s3://$bucket_name/s3-copied.txt" >/dev/null # Verify object is gone list_after_delete=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json) copied_gone=$(echo "$list_after_delete" | \ - jq -r '[.Contents[]? | .Key] | if any(. == "s3-copied.txt") then "false" else "true" end' 2>/dev/null) + jq_r '[.Contents[]? | .Key] | if any(. == "s3-copied.txt") then "false" else "true" end') check "Deleted object no longer listed" "true" "$copied_gone" # --------------------------------------------- @@ -203,7 +216,7 @@ check "Multipart upload (7MB)" "true" "$large_ok" # Verify size via HeadObject large_head=$(s3 s3api head-object --bucket "$bucket_name" --key "large-file.bin" --output json) -remote_size=$(echo "$large_head" | jq -r '.ContentLength // 0' 2>/dev/null) +remote_size=$(echo "$large_head" | jq_r '.ContentLength // 0') check "Multipart upload size matches ($large_size bytes)" "$large_size" "$remote_size" # Download and verify size @@ -248,7 +261,7 @@ s3 s3 cp "$presign_file" "s3://$bucket_name/presign-test.txt" >/dev/null rm -f "$presign_file" presigned_url=$(s3 s3 presign "s3://$bucket_name/presign-test.txt") -presign_body=$(curl -s "$presigned_url") +presign_body=$(curl -s "$presigned_url" || true) check "Presigned URL returns correct content" "presigned content test" "$presign_body" # --------------------------------------------- @@ -276,7 +289,7 @@ s3 s3 rm "s3://$bucket_name/" --recursive >/dev/null s3 s3api delete-bucket --bucket "$bucket_name" >/dev/null # Verify bucket is gone bucket_gone=$(s3 s3api list-buckets --output json | \ - jq -r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end' 2>/dev/null) + jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end') check "Bucket deleted via S3" "true" "$bucket_gone" # --------------------------------------------- From 18bad2e61c748735c8a96166b3604fb5c85545a2 Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:20:22 +0200 Subject: [PATCH 131/141] chore(self-hosted): remove em-dashes (#48498) --- docker/tests/test-s3-backend.sh | 4 ++-- docker/tests/test-s3.sh | 4 ++-- docker/tests/test-self-hosted.sh | 2 +- docker/volumes/api/kong.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker/tests/test-s3-backend.sh b/docker/tests/test-s3-backend.sh index c7b31a12c37..9c0d619a6fa 100644 --- a/docker/tests/test-s3-backend.sh +++ b/docker/tests/test-s3-backend.sh @@ -4,7 +4,7 @@ # # Validates that the S3-compatible backend (MinIO, RustFS, etc.) handles # all S3 operations that Storage relies on. Uses the aws cli so the test -# is backend-agnostic — no vendor-specific tools required. +# is backend-agnostic - no vendor-specific tools required. # # Usage: # sh test-s3-backend.sh # Uses localhost:9100 @@ -82,7 +82,7 @@ s3() { } # Wrapper for jq that yields empty output instead of aborting the suite when -# the payload is not JSON — an S3 error document, say — and jq exits non-zero. +# the payload is not JSON (e.g. an S3 error document) and jq exits non-zero. jq_r() { jq -r "$@" 2>/dev/null || true } diff --git a/docker/tests/test-s3.sh b/docker/tests/test-s3.sh index 9810afe27fb..38db52c298a 100644 --- a/docker/tests/test-s3.sh +++ b/docker/tests/test-s3.sh @@ -3,7 +3,7 @@ # Test S3 protocol endpoint for self-hosted Supabase Storage. # # Verifies that the S3-compatible endpoint at /storage/v1/s3 works with -# standard S3 clients — the same way end users interact with it via +# standard S3 clients - the same way end users interact with it via # aws cli, rclone, or other S3-compatible tools. # # Usage: @@ -80,7 +80,7 @@ s3() { } # Wrapper for jq that yields empty output instead of aborting the suite when -# the payload is not JSON — an S3 error document, say — and jq exits non-zero. +# the payload is not JSON (e.g. an S3 error document) and jq exits non-zero. jq_r() { jq -r "$@" 2>/dev/null || true } diff --git a/docker/tests/test-self-hosted.sh b/docker/tests/test-self-hosted.sh index a72638abdec..52b11cc0558 100644 --- a/docker/tests/test-self-hosted.sh +++ b/docker/tests/test-self-hosted.sh @@ -167,7 +167,7 @@ else check "Create user (admin)" "true" "false" fi -# Public signup (optional — depends on email autoconfirm setting) +# Public signup (optional - depends on email autoconfirm setting) signup_email="smoke-signup-$$@example.com" signup_resp=$(http_body "$BASE_URL/auth/v1/signup" \ -H "apikey: $ANON_KEY" \ diff --git a/docker/volumes/api/kong.yml b/docker/volumes/api/kong.yml index 13fb01e1fca..84433c13657 100644 --- a/docker/volumes/api/kong.yml +++ b/docker/volumes/api/kong.yml @@ -305,7 +305,7 @@ services: - anon ## Storage API endpoint (with Authorization header transformation). - ## No key-auth — S3 protocol requests don't carry an apikey header. + ## No key-auth - S3 protocol requests don't carry an apikey header. ## ## The request-transformer translates opaque API keys to asymmetric JWTs ## and passes through existing Authorization headers (user JWTs, AWS SigV4). From 4466cc4e456aabd487699e0873b415f7a076478f Mon Sep 17 00:00:00 2001 From: "Andrey A." <56412611+aantti@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:29:12 +0200 Subject: [PATCH 132/141] fix(self-hosted): override dns ttl in kong config to avoid wrong routing (#47846) --- docker/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 80ab7c50761..12b7efe9d05 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -96,6 +96,7 @@ services: # https://github.com/supabase/cli/issues/14 KONG_DNS_ORDER: LAST,A,CNAME KONG_DNS_NOT_FOUND_TTL: 1 + KONG_DNS_VALID_TTL: 5 KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k From 3a1b1e391d9c889224fd27a15c185832ef150568 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:02:51 -0400 Subject: [PATCH 133/141] chore(design-system): match studio font stack (#48477) ## What kind of change does this PR introduce? Chore ## What is the current behavior? Design system still uses the old CustomFont family, so documented components no longer match Studio, www, and docs after the Inter / Manrope rollout. ## What is the new behavior? Design system uses the same font stack as the other apps: - Inter for body (`--font-sans`) - Manrope for headings (`--font-heading`) - Source Code Pro for mono Also adopts the Inter-optimised type scale and base weight (`450`) used in Studio. ## Additional context Based on Francesco's unshipped `chore/update-fonts` draft (design-system slice only). Related shipped work: #47306, #47227. ## Summary by CodeRabbit * **Style** * Updated the design system with refreshed typography using Inter and Manrope, with Source Code Pro for code. * Refined theme typography and spacing tokens, including a more complete text scale, updated font weights, and container sizing. * Improved heading styling and ensured `code`/`pre`/`kbd`/`samp` use consistent monospace treatment. * Adjusted default border styling and improved font loading/fallback behavior for more consistent rendering. --- .../app/fonts/CustomFont-Black.woff2 | Bin 28432 -> 0 bytes .../app/fonts/CustomFont-BlackItalic.woff2 | Bin 31208 -> 0 bytes .../app/fonts/CustomFont-Bold.woff2 | Bin 28564 -> 0 bytes .../app/fonts/CustomFont-BoldItalic.woff2 | Bin 31308 -> 0 bytes .../app/fonts/CustomFont-Book.woff2 | Bin 24964 -> 0 bytes .../app/fonts/CustomFont-BookItalic.woff2 | Bin 27016 -> 0 bytes .../app/fonts/CustomFont-Medium.woff2 | Bin 28204 -> 0 bytes apps/design-system/app/fonts/index.ts | 53 --------------- apps/design-system/app/layout.tsx | 6 +- apps/design-system/lib/fonts.ts | 23 +++++++ apps/design-system/styles/globals.css | 62 +++++++++++++++++- 11 files changed, 85 insertions(+), 59 deletions(-) delete mode 100644 apps/design-system/app/fonts/CustomFont-Black.woff2 delete mode 100644 apps/design-system/app/fonts/CustomFont-BlackItalic.woff2 delete mode 100644 apps/design-system/app/fonts/CustomFont-Bold.woff2 delete mode 100644 apps/design-system/app/fonts/CustomFont-BoldItalic.woff2 delete mode 100644 apps/design-system/app/fonts/CustomFont-Book.woff2 delete mode 100644 apps/design-system/app/fonts/CustomFont-BookItalic.woff2 delete mode 100644 apps/design-system/app/fonts/CustomFont-Medium.woff2 delete mode 100644 apps/design-system/app/fonts/index.ts create mode 100644 apps/design-system/lib/fonts.ts diff --git a/apps/design-system/app/fonts/CustomFont-Black.woff2 b/apps/design-system/app/fonts/CustomFont-Black.woff2 deleted file mode 100644 index e3c834e57f291b97c981c6adfdb2630d6dc7427f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28432 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 DxOWet diff --git a/apps/design-system/app/fonts/CustomFont-BlackItalic.woff2 b/apps/design-system/app/fonts/CustomFont-BlackItalic.woff2 deleted file mode 100644 index f84036283ec2ead6430ffaf43c9e6b9a55b88a92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31208 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 TIJPo}*2k5>Rhc6lm8}#2BQQY~ diff --git a/apps/design-system/app/fonts/CustomFont-Book.woff2 b/apps/design-system/app/fonts/CustomFont-Book.woff2 deleted file mode 100644 index abd31f7ec272f3fd46a87d7431bb2c243ee604c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24964 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 diff --git a/apps/design-system/app/fonts/CustomFont-Medium.woff2 b/apps/design-system/app/fonts/CustomFont-Medium.woff2 deleted file mode 100644 index c07131ddec93a046153a0cba71df6f751eea7acd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28204 zcmV)HK)t_rPew8T0RR910B$S*4*&oF0a3I70By?v0RR9100000000000000000000 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 jsghaUSONz*0IZ$1WibRPHVDex+9) diff --git a/apps/design-system/lib/fonts.ts b/apps/design-system/lib/fonts.ts new file mode 100644 index 00000000000..91e4a08c3ee --- /dev/null +++ b/apps/design-system/lib/fonts.ts @@ -0,0 +1,23 @@ +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({ + subsets: ['latin'], + fallback: ['Source Code Pro', 'Office Code Pro', 'Menlo', 'monospace'], + variable: '--font-source-code-pro', + display: 'swap', + weight: ['400', '500', '600', '700'], +}) diff --git a/apps/design-system/styles/globals.css b/apps/design-system/styles/globals.css index 0418cb7fedf..32c9d5d396b 100644 --- a/apps/design-system/styles/globals.css +++ b/apps/design-system/styles/globals.css @@ -9,9 +9,30 @@ @source './../../../packages/ui/src/**/*.{tsx,ts,js}'; @source './../../../packages/ui-patterns/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; +} + @theme { /* added to get max-w-site */ --container-site: 128rem; + /* 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; } @layer base { @@ -30,9 +51,7 @@ --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; } -} -@layer base { * { @apply border-border; } @@ -40,13 +59,35 @@ @apply scroll-smooth; } body { - @apply bg-default text-foreground; + @apply bg-default text-foreground font-normal; /* font-feature-settings: "rlig" 1, "calt" 1; */ font-synthesis-weight: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + + code, + .code-content, + pre, + kbd, + samp, + .font-mono { + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + --text-6xl: 3.75rem; + --text-7xl: 4.5rem; + --text-8xl: 6rem; + --text-9xl: 8rem; + --font-weight-normal: 400; + } } @layer utilities { @@ -76,6 +117,21 @@ } } +h1:not(.font-mono), +h2:not(.font-mono), +h3:not(.font-mono), +h4:not(.font-mono), +h5:not(.font-mono), +h6:not(.font-mono), +.h1:not(.font-mono), +.h2:not(.font-mono), +.h3:not(.font-mono), +.h4:not(.font-mono), +.h5:not(.font-mono), +.h6:not(.font-mono) { + @apply font-heading font-semibold; +} + .rdg-cell { padding-inline: 0.5rem; } From 205527c716d87509f582469b6e24085f74a80e56 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:20:38 -0400 Subject: [PATCH 134/141] feat(studio): tune up replication page layout and empty state (#48265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? UI polish for Database → Replication: empty state, page layout, diagram containment, filter actions, and destination sheet copy. ## What is the current behavior? - Empty destinations use a custom dashed box with generic copy. - The diagram is full-bleed with `border-y`. - Page chrome still uses legacy Scaffold wrappers and a long header description that duplicates the empty state. - Filter-row “Add destination” is a default button ahead of secondary actions; Usage always shows. - Local ETL warning sits on the page list. - Destination sheet title/helper/access copy is more verbose; Type helper sits beside the label. ## What is the new behavior? - Empty state uses `EmptyStatePresentational` with clearer CTA copy; toolbar “Add destination” is primary at the end of the filter row; empty-state CTA stays default. - Usage only appears when there is at least one Pipelines destination. - Page uses `PageHeader` / `PageContainer` / `PageSection`, with a short header: “Read replicas and analytics pipelines”. - Diagram sits in a rounded bordered frame inside the page container (same treatment as project overview). - Local ETL warning shows in the add-destination sheet for Pipelines types only, without blocking the form; Type + warning scroll with the sheet body. - Type helper sits under the dropdown as one flowing paragraph (“Cannot be changed after creation…” plus shortened stage notice). - Sheet title is “Add destination”; close only fires on dismiss; Lucide icons (e.g. Snowflake) use consistent stroke width. | Before | After | | --- | --- | | Replication Database Chisel
Toolshed Supabase | Replication Database Chisel
Toolshed Supabase | | Replication Database Chisel
Toolshed Supabase | Replication Database Chisel
Toolshed Supabase | ### Test plan - [x] Open **Database → Replication** with no replicas/destinations: empty state, primary filter-row Add destination, default empty-state Add destination, no Usage button. - [x] Confirm header→diagram and diagram→filter spacing feel even; filter→empty state/table stays tight (`space-y-4`). - [x] Confirm diagram is contained (rounded border), not full-bleed. - [x] Add a Pipelines destination (or use a project that has one): Usage appears; table still works; search “no results” still works. - [ ] Open **Add destination**: title “Add destination”; Type helper under the dropdown; Read Replica has no local warning; Pipelines type shows local warning without hiding fields (when ETL isn’t set up locally). - [ ] Without Pipelines access: request-access panel shows “alpha” copy and “Request alpha access”. - [ ] Spot-check dark/light themes. --------- Co-authored-by: Joshen Lim --- .../DestinationForm/index.tsx | 5 +- .../DestinationPanel/DestinationPanel.tsx | 134 +++++++++----- .../DestinationTypeSelection.test.tsx | 2 +- .../DestinationTypeSelection.tsx | 61 +++--- .../ReadReplicaForm/index.tsx | 17 +- .../Database/Replication/Destinations.tsx | 174 +++++++++--------- .../EmptyReplicationDiagram.tsx | 2 +- .../Replication/ReplicationDiagram/index.tsx | 2 +- .../[ref]/database/replication/index.tsx | 53 +++--- 9 files changed, 257 insertions(+), 193 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx index 69644ce02b0..a88710f4b53 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx @@ -3,7 +3,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { AnimatePresence, motion } from 'framer-motion' import { Loader2 } from 'lucide-react' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { useForm, useWatch } from 'react-hook-form' import { AWS_REGIONS } from 'shared-data' import { toast } from 'sonner' @@ -82,6 +82,7 @@ interface DestinationFormProps { selectedType: DestinationType visible: boolean existingDestination?: ExistingDestination + typeSelection?: ReactNode onClose: () => void } @@ -89,6 +90,7 @@ export const DestinationForm = ({ selectedType, visible, existingDestination, + typeSelection, onClose, }: DestinationFormProps) => { const { ref: projectRef } = useParams() @@ -422,6 +424,7 @@ export const DestinationForm = ({ return ( <> + {typeSelection} {hasNoAvailableDestinations && !editMode ? ( ) : ( diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx index a6e9f94be47..d0120d9ad59 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx @@ -1,3 +1,4 @@ +import { useParams } from 'common' import { ArrowUpRight } from 'lucide-react' import Link from 'next/link' import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs' @@ -14,6 +15,7 @@ import { SheetSection, SheetTitle, } from 'ui' +import { Admonition } from 'ui-patterns/Admonition' import { EnablePipelinesCallout } from '../EnablePipelinesCallout' import { PipelineStatusName } from '../Replication.constants' @@ -24,6 +26,8 @@ import { DestinationType } from './DestinationPanel.types' import { DestinationTypeSelection } from './DestinationTypeSelection' import { ReadReplicaForm } from './ReadReplicaForm' import { DocsButton } from '@/components/ui/DocsButton' +import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query' +import { checkLocalETLNotSetUp } from '@/data/replication/utils' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { DOCS_URL } from '@/lib/constants' @@ -32,8 +36,11 @@ interface DestinationPanelProps { } export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => { + const { ref: projectRef } = useParams() const enablePgReplicate = useIsETLPrivateAlpha() const { hasAccess: hasETLReplicationAccess } = useCheckEntitlements('replication.etl') + const { error: destinationsError } = useReplicationDestinationsQuery({ projectRef }) + const isLocalETLNotSetUp = checkLocalETLNotSetUp(destinationsError) const [urlDestinationType, setDestinationType] = useQueryState( 'destinationType', @@ -89,7 +96,7 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane } const docsUrl = - urlDestinationType === 'BigQuery' + destinationType === 'BigQuery' ? `${DOCS_URL}/guides/database/replication/bigquery#configure-bigquery-as-a-destination` : `${DOCS_URL}/guides/database/replication/pipelines#step-3-configure-a-destination` @@ -100,71 +107,108 @@ export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPane } }, [edit, invalidExistingDestination, setEdit]) + const typeSelection = ( + <> + + + + ) + + const pipelinesTypeSelection = ( + <> + {typeSelection} + {destinationType != null && isLocalETLNotSetUp && ( + + + + )} + + ) + return ( <> - - -
    + { + if (!open) onClose() + }} + > + +
    - {editMode ? 'Edit destination' : 'Add new destination'} + {editMode ? 'Edit destination' : 'Add destination'} {editMode ? 'Update the configuration for this destination.' - : 'Set up a read replica or Supabase Pipelines destination for near real-time replication'} + : 'Add a read replica or an external destination.'}
    - +
    - - - - {destinationType === 'Read Replica' ? ( - onSuccessCreateReadReplica?.()} /> + onSuccessCreateReadReplica?.()} + /> ) : !enablePgReplicate ? ( - -
    -
    -

    Request Pipelines access

    -

    - Pipelines is in public alpha and - being rolled out gradually. Request access below to join the waitlist. Read - replicas are available now. -

    -
    -
    - - + + Request Pipelines access + + + +
    -
    - + +
    ) : replicationNotEnabled ? ( - - - +
    + {pipelinesTypeSelection} + + + +
    ) : ( )} diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.test.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.test.tsx index 7d62e31f8c6..9ef30c6a292 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.test.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.test.tsx @@ -132,7 +132,7 @@ describe('DestinationTypeSelection', () => { fireEvent.click(await screen.findByRole('combobox')) fireEvent.click(await screen.findByText('BigQuery')) - expect(await screen.findByText(/This destination type is in public alpha/)).toBeInTheDocument() + expect(await screen.findByText(/In public alpha and may change/)).toBeInTheDocument() }) test('disables the selector in edit mode so the destination type cannot be changed', async () => { diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx index 6761906bc21..792e0f9f837 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationTypeSelection.tsx @@ -1,6 +1,7 @@ import { AnalyticsBucket, BigQuery, ClickHouse, Database } from 'icons' import { Snowflake } from 'lucide-react' import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs' +import type { ElementType } from 'react' import { Badge, Select, @@ -29,7 +30,7 @@ interface DestinationTypeOption { value: DestinationType label: string description: string - icon: typeof Database + icon: ElementType<{ size?: number; className?: string; strokeWidth?: number }> stage: 'Public Alpha' | 'Early Access' | 'Deprecated' | null enabled: boolean } @@ -39,6 +40,12 @@ interface DestinationTypeGroup { options: DestinationTypeOption[] } +function DestinationOptionIcon({ option }: { option: DestinationTypeOption }) { + const Icon = option.icon + + return +} + export const DestinationTypeSelection = () => { const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha() const etlEnableIceberg = useIsETLIcebergPrivateAlpha() @@ -147,29 +154,41 @@ export const DestinationTypeSelection = () => { .flatMap((group) => group.options) .find((option) => option.value === destinationType) + const stageDescription = + selectedOption?.stage === 'Public Alpha' ? ( + <> + In public alpha and may change.{' '} + + Leave feedback + + + ) : selectedOption?.stage === 'Early Access' ? ( + <> + In early access and may change.{' '} + + Leave feedback + + + ) : selectedOption?.stage === 'Deprecated' ? ( + 'This destination type is deprecated.' + ) : null + + const typeDescription = + !editMode || stageDescription ? ( + + {!editMode && 'Cannot be changed after creation.'} + {!editMode && stageDescription ? ' ' : null} + {stageDescription} + + ) : undefined + return ( - {selectedOption.stage === 'Public Alpha' - ? 'This destination type is in public alpha and may change as we iterate based on customer feedback. ' - : selectedOption.stage === 'Deprecated' - ? 'This destination type is deprecated.' - : 'This destination type is available through early access and may change as we iterate based on customer feedback. '} - {selectedOption.stage !== 'Deprecated' && ( - - Leave feedback - - )} - - ) - } + description={typeDescription} > } - value={filterString} - className="w-full lg:w-52" - onChange={(e) => setFilterString(e.target.value)} - actions={ - filterString.length > 0 && ( - - - {organization?.slug && ( - - )} - - {canDisablePipelines && ( - - - + @@ -304,13 +314,6 @@ export const Destinations = () => { /> )} - {isLocalETLNotSetUp && ( - - )} - {isLoading ? ( ) : hasReplicas || hasDestinations ? ( @@ -368,27 +371,20 @@ export const Destinations = () => { ) : ( !isLoading && !hasErrorsFetchingData && ( -
    -

    Replication keeps your data in sync across systems

    -

    - Deploy Read Replicas for lower latency and workload isolation, or add a Pipelines - destination for analytics workloads. -

    -
    + ) )} @@ -399,6 +395,6 @@ export const Destinations = () => { open={showDisablePipelinesDialog} setOpen={setShowDisablePipelinesDialog} /> - + ) } diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/EmptyReplicationDiagram.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/EmptyReplicationDiagram.tsx index f9f861f6025..105e544adf7 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/EmptyReplicationDiagram.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationDiagram/EmptyReplicationDiagram.tsx @@ -30,7 +30,7 @@ const ReplicationDiagramContent = () => { resolvedTheme === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.4)' return ( -
    +
    { }, [nodes, isSuccessDestinations, skipRenderingDestinations, isSuccessReplicas]) return ( -
    +
    { return ( - - -
    -
    -

    Replication

    -
    -

    - Deploy Read Replicas across multiple regions, or use Pipelines to replicate database - changes to analytics destinations. -

    -
    -
    -
    + + + + Replication + Read replicas and analytics pipelines + + + - {isPending ? ( - + + {isPending ? ( - - ) : ( - <> - - - + ) : ( + + + - - - - )} + + + )} +
    ) } From 5edcaef74cfa9851c5c4c1496b5a033e50681060 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:23:24 -0400 Subject: [PATCH 135/141] chore: show organization invite errors inline (#48470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix and design-system documentation update. ## What is the current behavior? Invite acceptance failures only appear in a transient toast. ## What is the new behavior? Invite failures remain visible beside the actions. The design-system guidance now distinguishes field, action, state, and toast feedback. | Before | After | | --- | --- | | Join Organization Supabase | Join Organization Supabase | ## To test With local Studio running at `http://localhost:8082`: 1. Open `apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts`. 2. At line 37, immediately inside `getOrganizationInviteStatus`, add: ```tsx return 'ready' ``` This deliberately bypasses invite lookup and account checks for the visual test. 3. Open `apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx`. 4. At line 30, change: ```tsx const [joinError, setJoinError] = useState() ``` to: ```tsx const [joinError, setJoinError] = useState('Invite token can only be accepted via an SSO account') ``` 5. Open `http://localhost:8082/join?token=test&slug=test` while signed in. 6. Confirm the card says **Join an organization** and shows the error below **Decline**, separated from the actions by a divider. 7. Revert both temporary edits before committing anything. ## Additional context First PR in a five-PR stack. ## Summary by CodeRabbit - **New Features** - Added a new connect interstitial example showcasing an inline action-error state with clear retry guidance. - **Bug Fixes** - Invitation acceptance failures now show inline destructive feedback under “Accept invite,” keeping the button enabled for retry (and removing prior toast-based failure behavior). - Updated the invalid-invitation title to “Invalid invitation.” - Changed the “Decline” link destination to `/organizations`. - **Documentation** - Expanded Sonner toast “When to use” guidance. - Refined form and connect interstitial action-feedback patterns (inline vs toast usage). - **Tests** - Updated and added coverage for the inline error rendering and “Invalid invitation” text. --------- Co-authored-by: Joshen Lim --- apps/design-system/__registry__/index.tsx | 11 +++++ .../content/docs/components/sonner.mdx | 15 +++++++ .../ui-patterns/connect-interstitials.mdx | 42 +++++++++++++++++++ .../content/docs/ui-patterns/forms.mdx | 4 +- .../connect-interstitial-action-error.tsx | 36 ++++++++++++++++ apps/design-system/registry/examples.ts | 5 +++ .../OrganizationInvite.test.tsx | 18 +++++++- .../OrganizationInvite/OrganizationInvite.tsx | 32 ++++++++------ .../OrganizationInvite.utils.test.ts | 4 +- .../OrganizationInvite.utils.ts | 2 +- 10 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 apps/design-system/registry/default/example/connect-interstitial-action-error.tsx rename apps/studio/{tests/components => components/interfaces/OrganizationInvite}/OrganizationInvite.test.tsx (92%) rename apps/studio/{tests/components => components/interfaces/OrganizationInvite}/OrganizationInvite.utils.test.ts (96%) diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index fade8897fb5..7e9ec446ef2 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -2623,6 +2623,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "connect-interstitial-action-error": { + name: "connect-interstitial-action-error", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/connect-interstitial-action-error")), + source: "", + files: ["registry/default/example/connect-interstitial-action-error.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "connect-interstitial-logo-pair": { name: "connect-interstitial-logo-pair", type: "components:example", diff --git a/apps/design-system/content/docs/components/sonner.mdx b/apps/design-system/content/docs/components/sonner.mdx index 3ae4ce8ee0a..bfcae2524a2 100644 --- a/apps/design-system/content/docs/components/sonner.mdx +++ b/apps/design-system/content/docs/components/sonner.mdx @@ -58,6 +58,21 @@ import { toast } from 'sonner' toast('Event has been created.') ``` +## When to use + +Use a toast for short-lived, non-blocking feedback or to confirm an operation +after its originating surface has closed or navigated away. + +Do not use a toast as the only feedback for: + +- Field validation. Use `FormMessage` or `FieldError` beside the field. +- A failed submission that leaves the form or interstitial visible. Show a + `role="alert"` message in destructive text near the actions. +- A blocking or materially changed page state. Use an inline state component + such as `Admonition` or `ErrorDisplay`. + +Keep error copy specific and include the next step when it is not obvious. + ## Expand You can change the amount of toasts visible through the visibleToasts prop. diff --git a/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx b/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx index ceb6d36b189..99c0671ea69 100644 --- a/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx +++ b/apps/design-system/content/docs/ui-patterns/connect-interstitials.mdx @@ -200,6 +200,48 @@ for new states instead of inventing a parallel card style. Prefer one full-width primary action. A full-width text button is fine for a secondary action that still belongs in the flow. +### Action feedback + +Match feedback to its scope: + +- Use `FormMessage` or `FieldError` beside a field when that field needs to + change. +- Show a submission or action failure as simple destructive text below the + actions. Separate it with a subtle divider when needed for composition. Keep + the current account, selections, and actions visible so the user can retry. +- Use `Admonition` when the whole interstitial is blocked or has materially + changed state, such as an invalid link, wrong account, or partially completed + setup. +- Use a toast only for non-blocking feedback or a completed action whose + originating surface is no longer visible. A toast must not be the only + feedback for a failure the user needs to resolve on the current card. + +```tsx +{ + actionError && ( +
    +

    + {actionError} +

    +
    + ) +} +``` + +Clear stale action feedback when the user retries or changes a relevant +selection. Error copy should say what failed and, when it is not obvious, what +the user can do next. + + + ## States Keep loading, invalid, error, and success states inside the same card when the diff --git a/apps/design-system/content/docs/ui-patterns/forms.mdx b/apps/design-system/content/docs/ui-patterns/forms.mdx index 305231b796b..add92626642 100644 --- a/apps/design-system/content/docs/ui-patterns/forms.mdx +++ b/apps/design-system/content/docs/ui-patterns/forms.mdx @@ -42,7 +42,7 @@ Keep repeated-row validation in the form schema or shared validation helper, not Build a custom row when the cells are mixed controls, such as an input paired with a `Select`. -## Best Practices +## Best practices 1. **Always use FormItemLayout**: Use `FormItemLayout` instead of manually composing `FormItem`, `FormLabel`, `FormMessage`, and `FormDescription`. @@ -57,7 +57,7 @@ Build a custom row when the cells are mixed controls, such as an input paired wi 5. **Handle dirty state**: Show cancel buttons and disable save buttons based on `form.formState.isDirty`. Make sure you destructure `isDirty` from `form.formState` (see https://react-hook-form.com/docs/useform/formstate) -6. **Error handling**: Always use mutations with `onSuccess` and `onError` callbacks that show toast notifications. +6. **Error handling**: Match feedback to its scope. Use `FormMessage` or `FieldError` for field validation. Show submission failures inline near the form actions when the user needs to retry or change something. Reserve toasts for non-blocking feedback or completed operations whose originating surface is no longer visible. 7. **Loading states**: Show loading states on submit buttons using the `loading` prop. diff --git a/apps/design-system/registry/default/example/connect-interstitial-action-error.tsx b/apps/design-system/registry/default/example/connect-interstitial-action-error.tsx new file mode 100644 index 00000000000..085b3a48678 --- /dev/null +++ b/apps/design-system/registry/default/example/connect-interstitial-action-error.tsx @@ -0,0 +1,36 @@ +import { Button } from 'ui' + +import { + AccountRow, + InterstitialShell, + LogoPair, + StripeLogo, + SupabaseLogo, +} from './connect-interstitial-shared' + +export default function ConnectInterstitialActionError() { + return ( + } right={} />} + title="Authorize Stripe Projects" + description="This will create an organization on your behalf in Supabase" + > +
    + +
    + + +
    +

    + Failed to authorize Stripe Projects. Please try again. +

    +
    +
    +
    +
    + ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 2697a4f176e..04b6a3ec5b6 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -1408,6 +1408,11 @@ export const examples: Registry = [ type: 'components:example', files: ['example/connect-interstitial-demo.tsx'], }, + { + name: 'connect-interstitial-action-error', + type: 'components:example', + files: ['example/connect-interstitial-action-error.tsx'], + }, { name: 'connect-interstitial-logo-pair', type: 'components:example', diff --git a/apps/studio/tests/components/OrganizationInvite.test.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.test.tsx similarity index 92% rename from apps/studio/tests/components/OrganizationInvite.test.tsx rename to apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.test.tsx index baad458e0e9..f6f4201b7cc 100644 --- a/apps/studio/tests/components/OrganizationInvite.test.tsx +++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.test.tsx @@ -2,7 +2,7 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, test, vi } from 'vitest' -import { OrganizationInvite } from '@/components/interfaces/OrganizationInvite/OrganizationInvite' +import { OrganizationInvite } from './OrganizationInvite' import type { OrganizationInviteByToken } from '@/data/organization-members/organization-invitation-token-query' import type { ProfileContextType } from '@/lib/profile' import { render } from '@/tests/helpers' @@ -172,6 +172,20 @@ describe('OrganizationInvite', () => { }) }) + test('renders an inline error when joining the organization fails', () => { + mocks.useAcceptInvitationMutation.mockReturnValue({ + mutate: mocks.acceptInvitation, + isPending: false, + error: responseError('You are already a member of this organization'), + }) + + render() + + expect(screen.getByRole('alert')).toHaveTextContent( + 'Failed to join organization: You are already a member of this organization' + ) + }) + test('renders a wrong-account warning and signs out', async () => { const user = userEvent.setup() mocks.useInvitationQuery.mockReturnValue({ @@ -225,7 +239,7 @@ describe('OrganizationInvite', () => { rerender() - expect(screen.getByText('Invite invalid')).toBeInTheDocument() + expect(screen.getByText('Invalid invitation')).toBeInTheDocument() expect( screen.getByText( 'Open the full invite link again, or ask the organization owner for a new invite.' diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx index 6b150756a5d..7e62eb43785 100644 --- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx +++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.tsx @@ -1,8 +1,7 @@ import { useIsLoggedIn, useParams } from 'common' import Link from 'next/link' import { useRouter } from 'next/router' -import type { ReactNode } from 'react' -import { toast } from 'sonner' +import { type ReactNode } from 'react' import { Button, Card, CardContent } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' @@ -25,9 +24,9 @@ import { useProfile, useProfileNameAndPicture } from '@/lib/profile' export const OrganizationInvite = () => { const router = useRouter() const isLoggedIn = useIsLoggedIn() + const { slug, token } = useParams() const { profile, isLoading: isLoadingProfile } = useProfile() const { username, avatarUrl, primaryEmail } = useProfileNameAndPicture() - const { slug, token } = useParams() const isSignUpEnabled = useIsFeatureEnabled('dashboard_auth:sign_up') @@ -70,15 +69,17 @@ export const OrganizationInvite = () => { const mfaRequiredError = error?.message.includes('MFA required') - const { mutate: joinOrganization, isPending: isJoining } = - useOrganizationAcceptInvitationMutation({ - onSuccess: () => { - router.push('/organizations') - }, - onError: (error) => { - toast.error(`Failed to join organization: ${error.message}`) - }, - }) + const { + mutate: joinOrganization, + isPending: isJoining, + error: joinError, + } = useOrganizationAcceptInvitationMutation({ + onSuccess: () => { + router.push('/organizations') + }, + // [Joshen] Silence the default toast handler + onError: () => {}, + }) async function handleJoinOrganization() { if (!slug) return console.error('Slug is required') @@ -196,6 +197,13 @@ export const OrganizationInvite = () => { + {joinError && ( +
    +

    + Failed to join organization: {joinError.message} +

    +
    + )}
    ) diff --git a/apps/studio/tests/components/OrganizationInvite.utils.test.ts b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.test.ts similarity index 96% rename from apps/studio/tests/components/OrganizationInvite.utils.test.ts rename to apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.test.ts index c119fb46531..6d6c7cc7c8d 100644 --- a/apps/studio/tests/components/OrganizationInvite.utils.test.ts +++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.test.ts @@ -4,7 +4,7 @@ import { getOrganizationInviteContent, getOrganizationInviteStatus, type OrganizationInviteStatus, -} from '@/components/interfaces/OrganizationInvite/OrganizationInvite.utils' +} from './OrganizationInvite.utils' import type { OrganizationInviteByToken } from '@/data/organization-members/organization-invitation-token-query' import type { ResponseError } from '@/types' @@ -101,7 +101,7 @@ describe('OrganizationInvite utils', () => { ['ready', 'Join Acme Corp', 'You have been invited to join this Supabase organization'], ['wrong-account', 'Wrong account', undefined], ['expired', 'Invite expired', undefined], - ['invalid', 'Invite invalid', undefined], + ['invalid', 'Invalid invitation', undefined], ['no-longer-valid', 'Invite no longer available', undefined], ['error', 'Unable to load invitation', undefined], ])('returns content for %s', (status, title, description) => { diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts index 8738739589b..53526db5ccf 100644 --- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts +++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInvite.utils.ts @@ -88,7 +88,7 @@ export function getOrganizationInviteContent({ if (status === 'wrong-account') return { title: 'Wrong account' } if (status === 'expired') return { title: 'Invite expired' } - if (status === 'invalid') return { title: 'Invite invalid' } + if (status === 'invalid') return { title: 'Invalid invitation' } if (status === 'no-longer-valid') return { title: 'Invite no longer available' } if (status === 'error') { if (error?.message.includes('MFA required')) { From 3b06c6c7cc0bf17a5a7337948ba7cb97a52b3631 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:34:05 -0400 Subject: [PATCH 136/141] fix(docs): unify docs card hover and retire IconPanel (#48379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix / docs UI polish. ## What is the current behavior? - Many docs `GlassPanel`s use `background={false}`, so hover only tweaks the border and reads as having no hover state - Compact icon+label grids still use `IconPanel`, which has a broken `-z-10` hover fill and overlaps with the newer `IconLink` pattern - Description card grids jump to 3-up too early on medium widths ## What is the new behavior? **GlassPanel** - Removes the `background` prop; cards always use the filled surface with stronger border hover - Tightens icon→description gap (`gap-6` → `gap-3`) - Decorative icons/logos use empty `alt` so screen readers don’t hear the title twice **Icon tiles** - Retires `IconPanel` from docs and deletes it from `ui-patterns` - Uses `IconLink` / `IconLinkList` for compact navigation tiles (auth providers, social login, etc.) - Adds `IconLinkButton` for SMS provider pickers (same chrome, opens a dialog) - Adds focus styles, list labelling, and dialog-trigger ARIA where needed **Layout / content** - Migrate-to-Supabase description cards on resources use `GlassPanel` (not slim icon tiles) - Grid spans use `md:… xl:…` so cards stay 2-up until ~1280px - Fixes migrate links to `/guides/platform/migrating-to-supabase/…` and SSR quickstarts to `creating-a-client` with framework query params - Moves the Extensions list `key` onto the outer `Link` | Before | After | | --- | --- | | Resources Supabase Docs | Resources Supabase Docs | ## Additional context Homepage compact sections already use `IconLinkList` from #48317; this PR finishes that pattern for remaining docs `IconPanel` callsites and cleans up GlassPanel hover. `www/customers` only drops the removed `background` prop; those cards already use the filled surface via `logo`. ## Test plan - [ ] `/guides/getting-started`: GlassPanels show filled surface and clearer border hover - [ ] `/guides/resources`: migrate cards are GlassPanels with working `/platform/…` links; 2-up until xl - [ ] `/guides/auth/social-login` and auth providers partial: IconLink tiles hover/focus correctly - [ ] `/guides/auth/phone-login`: SMS provider buttons open dialogs; keyboard focus works - [ ] Docs homepage: migrate / self-host IconLinkLists unchanged in behaviour - [ ] `/guides/auth/server-side`: Next.js / SvelteKit cards resolve on docs preview ## Summary by CodeRabbit * **Improved Layouts** * Made “GlassPanel” card grids more responsive and consistent; refined card and success badge spacing for a cleaner presentation. * **Updated Documentation** * Refreshed multiple guide and resource pages (including quickstarts and migration content) with standardized card layouts and updated link destinations. * **Component Updates** * Standardized “GlassPanel” styling (background toggle removed) and simplified icon-based panels; added an `IconLinkButton` for action tiles; updated authentication provider grids to use the shared tile UI. --- apps/docs/app/page.tsx | 22 +- apps/docs/components/AuthProviders.tsx | 42 +-- .../AuthSmsProviderConfig.tsx | 32 ++- .../ContentListings.client.tsx | 9 +- .../docs/components/Extensions/Extensions.tsx | 3 +- .../NavigationMenu.constants.ts | 6 +- apps/docs/content/_partials/providers.mdx | 4 +- apps/docs/content/guides/ai.mdx | 28 +- apps/docs/content/guides/auth/auth-hooks.mdx | 10 +- apps/docs/content/guides/auth/server-side.mdx | 14 +- .../docs/content/guides/auth/social-login.mdx | 25 +- .../serverless-drivers.mdx | 8 +- apps/docs/content/guides/getting-started.mdx | 271 ++++++++---------- .../guides/platform/migrating-to-supabase.mdx | 1 - apps/docs/content/guides/resources.mdx | 192 ++++--------- apps/docs/features/docs/MdxBase.shared.tsx | 2 - apps/docs/features/ui/IconLink.tsx | 84 ++++-- .../internals/generate-guides-markdown.ts | 1 - apps/www/pages/customers.tsx | 8 +- packages/ui-patterns/package.json | 4 - packages/ui-patterns/src/GlassPanel/index.tsx | 35 +-- packages/ui-patterns/src/IconPanel/index.tsx | 115 -------- 22 files changed, 354 insertions(+), 562 deletions(-) delete mode 100644 packages/ui-patterns/src/IconPanel/index.tsx diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 76a6fb6b16a..f3b5ea7822a 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -396,18 +396,16 @@ const HomePage = () => (
    -
    - ({ - title: option.title, - href: option.href, - icon: , - }))} - /> -
    + ({ + title: option.title, + href: option.href, + icon: , + }))} + /> )} diff --git a/apps/docs/components/AuthProviders.tsx b/apps/docs/components/AuthProviders.tsx index c9b1ae80263..15d6888b66c 100644 --- a/apps/docs/components/AuthProviders.tsx +++ b/apps/docs/components/AuthProviders.tsx @@ -1,24 +1,30 @@ -import { IconPanel } from 'ui-patterns/IconPanel' import providers from '../data/authProviders' -import Link from 'next/link' +import { IconLinkImage, IconLinkList } from '@/features/ui/IconLink' -export default function AuthProviders({ type }: { type: string }) { - const filterProviders = providers.filter((item) => item.authType === type) +export default function AuthProviders({ + type, + labelledBy, + label, +}: { + type: string + labelledBy?: string + label?: string +}) { + const items = providers + .filter((item) => item.authType === type) + .map((provider) => ({ + title: provider.name, + href: provider.href, + icon: , + })) return ( - <> -
    - {filterProviders.map((x) => ( - - - - ))} -
    - + ) } diff --git a/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx b/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx index 1bba14fa98a..0ab67c6c37a 100644 --- a/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx +++ b/apps/docs/components/AuthSmsProviderConfig/AuthSmsProviderConfig.tsx @@ -1,15 +1,15 @@ 'use client' import { safeHistoryReplaceState } from '~/lib/historyUtils' -import { useEffect, useReducer, useRef } from 'react' +import { useEffect, useId, useReducer, useRef } from 'react' import { Dialog, DialogContent, DialogHeader, DialogSection, Heading } from 'ui' -import { IconPanel } from 'ui-patterns/IconPanel' import { PhoneLoginsItems } from '../Navigation/NavigationMenu/NavigationMenu.constants' import MessageBird from './MessageBirdConfig.mdx' import TextLocal from './TextLocalConfig.mdx' import Twilio from './TwilioConfig.mdx' import Vonage from './VonageConfig.mdx' +import { IconLinkButton, IconLinkImage } from '@/features/ui/IconLink' const reducer = (_, action: (typeof PhoneLoginsItems)[number] | undefined) => { const url = new URL(document.location.href) @@ -24,6 +24,8 @@ const reducer = (_, action: (typeof PhoneLoginsItems)[number] | undefined) => { const AuthSmsProviderConfig = () => { const [selectedProvider, setSelectedProvider] = useReducer(reducer, undefined) + const dialogId = useId() + const dialogTitleId = useId() useEffect(() => { const providerName = new URLSearchParams(document.location.search ?? '').get('showSmsProvider') @@ -41,22 +43,20 @@ const AuthSmsProviderConfig = () => {

    Configuring SMS Providers

    -
    +
      {PhoneLoginsItems.map((provider) => ( - + ))} -
    + { > {selectedProvider && ( { evt.preventDefault() headingRef.current?.focus() }} > - + {selectedProvider.name} diff --git a/apps/docs/components/ContentListings/ContentListings.client.tsx b/apps/docs/components/ContentListings/ContentListings.client.tsx index 7e7ff213c85..f71fd0236b1 100644 --- a/apps/docs/components/ContentListings/ContentListings.client.tsx +++ b/apps/docs/components/ContentListings/ContentListings.client.tsx @@ -16,9 +16,10 @@ import { Heading } from 'ui/src/components/CustomHTMLElements' import { resolveContentListingIcon } from './iconChip' const GRID_ITEM_CLASS = { + // Stay 2-up until xl (~1280px) so cards aren't cramped beside the docs sidebar. 2: 'col-span-12 md:col-span-6', - 3: 'col-span-12 md:col-span-4', - 4: 'col-span-12 md:col-span-3', + 3: 'col-span-12 md:col-span-6 xl:col-span-4', + 4: 'col-span-12 md:col-span-6 xl:col-span-3', } as const function useContentListingClickHandler(group: ContentListingGroup) { @@ -88,9 +89,7 @@ function ContentListingsGroup({ group }: { group: ContentListingGroup }) { } > {item.badge && item.badgePosition === 'below' && ( - // Pull the badge up close to the title (GlassPanel's icon-row gap is - // large by default), then push extra space before the description below. - + {item.badge} )} diff --git a/apps/docs/components/Extensions/Extensions.tsx b/apps/docs/components/Extensions/Extensions.tsx index ce266aa4411..27d8dae1f0e 100644 --- a/apps/docs/components/Extensions/Extensions.tsx +++ b/apps/docs/components/Extensions/Extensions.tsx @@ -109,11 +109,12 @@ export default function Extensions() { ) .map((extension) => ( - +

    {extension.comment.charAt(0).toUpperCase() + extension.comment.slice(1)}

    diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 96e6899ff95..70effd01a41 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1,8 +1,6 @@ // End of third-party imports import { isFeatureEnabled } from 'common/enabled-features' -import type { ComponentProps } from 'react' -import type { IconPanel } from 'ui-patterns/IconPanel' import type { GlobalMenuItems, NavMenuConstant, NavMenuSection } from '../Navigation.types' @@ -2527,7 +2525,9 @@ export const contributing: NavMenuConstant = { items: [{ name: 'Overview', url: '/contributing' }], } -export const MIGRATION_PAGES: Partial>[] = [ +export const MIGRATION_PAGES: Partial< + NavMenuSection & { icon?: string; hasLightIcon?: boolean } +>[] = [ { name: 'Auth0', icon: '/docs/img/icons/auth0-icon', diff --git a/apps/docs/content/_partials/providers.mdx b/apps/docs/content/_partials/providers.mdx index 0c65a75ab1a..94f42414eb4 100644 --- a/apps/docs/content/_partials/providers.mdx +++ b/apps/docs/content/_partials/providers.mdx @@ -4,7 +4,7 @@ Supabase Auth works with many popular Auth methods, including Social and Phone A ### Social Auth - + @@ -14,4 +14,4 @@ You can also add any OAuth2 or OIDC-compatible identity provider using [Custom O ### Phone Auth - + diff --git a/apps/docs/content/guides/ai.mdx b/apps/docs/content/guides/ai.mdx index 5226d8849aa..263f0a17ae4 100644 --- a/apps/docs/content/guides/ai.mdx +++ b/apps/docs/content/guides/ai.mdx @@ -31,7 +31,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
    -
    +
    -
    +
    -
    +
    -
    +
    -
    +
    -
    +
    */}
    -
    +
    OpenAI is an AI research and deployment company. Supabase provides a way to use OpenAI in @@ -114,7 +114,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
    -
    +
    A fully managed service that offers a choice of high-performing foundation models from @@ -122,7 +122,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
    -
    +
    Hugging Face is an open-source provider of NLP technologies. Supabase provides a way to use @@ -130,7 +130,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
    -
    +
    LangChain is a language-agnostic, open-source, and self-hosted API for text translation, @@ -138,7 +138,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
    -
    +
    LlamaIndex is a data framework for your LLM applications. @@ -154,7 +154,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup {/* */}
    -
    +
    Learn how Berri AI overcame challenges with self-hosting their vector database on AWS RDS @@ -162,7 +162,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
    -
    +
    How Firecrawl boosts efficiency and accuracy of chat powered search for documentation using @@ -170,7 +170,7 @@ Check out all of the AI [templates and examples](https://github.com/supabase/sup
    -
    +
    AI-powered chatbot platform, Markprompt, empowers developers to deliver efficient and diff --git a/apps/docs/content/guides/auth/auth-hooks.mdx b/apps/docs/content/guides/auth/auth-hooks.mdx index b1259bc1d5a..327d87456d1 100644 --- a/apps/docs/content/guides/auth/auth-hooks.mdx +++ b/apps/docs/content/guides/auth/auth-hooks.mdx @@ -372,35 +372,35 @@ Outside of runtime errors, both HTTP Hooks and Postgres Hooks return timeout err Each Hook description contains an example JSON Schema which you can use in conjunction with [JSON Schema Faker](https://json-schema-faker.js.org/) in order to generate a mock payload. For HTTP Hooks, you can also use [the Standard Webhooks Testing Tool](https://www.standardwebhooks.com/simulate) to simulate a request.
    -
    +
    Customize the access token issued by Supabase Auth
    -
    +
    Use a custom SMS provider to send authentication messages
    -
    +
    Use a custom email provider to send authentication messages
    -
    +
    Add additional checks to the MFA verification flow
    -
    +
    Add additional checks to the password verification flow diff --git a/apps/docs/content/guides/auth/server-side.mdx b/apps/docs/content/guides/auth/server-side.mdx index 5a22ca7bbbf..9e93a926bb3 100644 --- a/apps/docs/content/guides/auth/server-side.mdx +++ b/apps/docs/content/guides/auth/server-side.mdx @@ -20,14 +20,20 @@ We have developed an [`@supabase/ssr`](https://www.npmjs.com/package/@supabase/s ## Framework quickstarts
    - - + + Automatically configure Supabase in Next.js to use cookies, making your user and their session available on the client and server. - - + + Automatically configure Supabase in SvelteKit to use cookies, making your user and their session available on the client and server. diff --git a/apps/docs/content/guides/auth/social-login.mdx b/apps/docs/content/guides/auth/social-login.mdx index 5f7cfe3ab7b..6433597e583 100644 --- a/apps/docs/content/guides/auth/social-login.mdx +++ b/apps/docs/content/guides/auth/social-login.mdx @@ -20,30 +20,7 @@ There are several reasons why you might want to add social login to your applica Supabase supports a suite of social providers. Follow these guides to configure a social provider for your platform. -
    - - {(data) => - data.map((item) => ( - - - {item.description} - - - )) - } - -
    + diff --git a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx index 88d0c416271..192b4ef7d8b 100644 --- a/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx +++ b/apps/docs/content/guides/database/connecting-to-postgres/serverless-drivers.mdx @@ -21,20 +21,20 @@ Choose one of these Vercel Deploy Templates which use our [Vercel Deploy Integra
    - + A Next.js App Router template configured with cookie-based auth using Supabase, TypeScript and Tailwind CSS. - + Basic Next.js template that uses Supabase as the database and Kysely as the query builder. diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index 163c49283da..317ce47a169 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -10,23 +10,26 @@ hideToc: true
    - - + + Develop with Supabase AI-first using plugins, MCP, and skills. - - + + Learn about the different API keys in Supabase and how to use them. - - + + Use the Supabase CLI to develop locally and collaborate between teams. @@ -39,11 +42,9 @@ hideToc: true ### Use cases
    - + @@ -53,28 +54,18 @@ hideToc: true - + Clone, deploy, and fully customize a SaaS subscription application with Next.js. - + Postgres full-text search, image storage, and more. @@ -85,138 +76,138 @@ hideToc: true <$Show if="docs:framework_quickstarts">
    - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a React app. - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a Next.js app. - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a Nuxt app. - - + + Learn how to create a Supabase project, add some sample data to your database, secure it with auth, and query the data from a Hono app. - - + + Learn how to create a Supabase project, add some sample data to your database using Prisma migration and seeds, and query the data from a RedwoodJS app. <$Show if="sdk:dart"> - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a Flutter app. <$Show if="sdk:swift"> - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from an iOS app. <$Show if="sdk:kotlin"> - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from an Android Kotlin app. - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a SvelteKit app. - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a SolidJS app. - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a Vue app. - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a TanStack Start app. - - + + Learn how to create a Supabase project, add some sample data to your database, and query the data from a Refine app. @@ -236,92 +227,74 @@ hideToc: true ### Web app demos
    - + Learn how to build a user management app with Next.js and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with React and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with Vue 3 and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with Nuxt 3 and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with Angular and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with RedwoodJS and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with Svelte and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with SvelteKit and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with Refine and Supabase Database, Auth, and Storage functionality. @@ -336,11 +309,9 @@ hideToc: true
    <$Show if="sdk:dart"> - + Learn how to build a user management app with Flutter and Supabase Database, Auth, and Storage functionality. @@ -350,12 +321,10 @@ hideToc: true @@ -365,12 +334,10 @@ hideToc: true @@ -378,11 +345,9 @@ hideToc: true <$Show if="sdk:kotlin"> - + Learn how to build a product management app with Android and Supabase Database, Auth, and Storage functionality. @@ -390,32 +355,26 @@ hideToc: true <$Show if="sdk:swift"> - + Learn how to build a user management app with iOS and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with Ionic React and Supabase Database, Auth, and Storage functionality. - + Learn how to build a user management app with Ionic Vue and Supabase Database, Auth, and Storage functionality. @@ -424,12 +383,10 @@ hideToc: true Learn how to build a user management app with Ionic Angular and Supabase Database, Auth, and Storage functionality. diff --git a/apps/docs/content/guides/platform/migrating-to-supabase.mdx b/apps/docs/content/guides/platform/migrating-to-supabase.mdx index f4715f672cf..aa9bab411f3 100644 --- a/apps/docs/content/guides/platform/migrating-to-supabase.mdx +++ b/apps/docs/content/guides/platform/migrating-to-supabase.mdx @@ -15,7 +15,6 @@ Learn how to migrate to Supabase from another database service. icon={page.icon} title={page.name} hasLightIcon={page.hasLightIcon} - background={false} className="[&>div]:p-4" /> diff --git a/apps/docs/content/guides/resources.mdx b/apps/docs/content/guides/resources.mdx index 3ebd7d7c6f8..6e29ac25665 100644 --- a/apps/docs/content/guides/resources.mdx +++ b/apps/docs/content/guides/resources.mdx @@ -11,13 +11,17 @@ hideToc: true
    - - + + Official GitHub examples, curated content from the community, and more. - - + + Definitions for terminology and acronyms used in the Supabase documentation. @@ -34,155 +38,94 @@ hideToc: true
    - + Move your auth users from Auth0 to a Supabase project. - + - + Move your auth users from a Firebase project to a Supabase project. - + - + Migrate the contents of a Firestore collection to a single Postgres table. - + - + Convert your Firebase Storage files to Supabase Storage. - + - + Migrate your Heroku Postgres database to Supabase. - + - + Migrate your Render Postgres database to Supabase. - + - + Migrate your Amazon RDS database to Supabase. - + - + Migrate your Postgres database to Supabase. - + - + Migrate your MySQL database to Supabase. - + - + Migrate your Microsoft SQL Server database to Supabase. - +
    @@ -196,59 +139,48 @@ hideToc: true
    - - + + Improve query performance using various index types in Postgres. - + Understand the types of foreign key constraint deletes. - + Delete all tables in a given schema. - + Retrieve the first row in each distinct group. - + Find out which version of Postgres you are running. diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index 7c9dc4372c7..78d5347b573 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -36,7 +36,6 @@ import { type ComponentPropsWithoutRef } from 'react' import { Badge, Button } from 'ui' import { Admonition, type AdmonitionProps } from 'ui-patterns/Admonition' import { GlassPanel } from 'ui-patterns/GlassPanel' -import { IconPanel } from 'ui-patterns/IconPanel' import SqlToRest from 'ui-patterns/SqlToRest' import { Heading } from 'ui/src/components/CustomHTMLElements' @@ -91,7 +90,6 @@ const components = { GlassPanel, IconArrowDown: ArrowDown, IconCheck: Check, - IconPanel, IconX: X, Image: (props: ImageProps) => , Link, diff --git a/apps/docs/features/ui/IconLink.tsx b/apps/docs/features/ui/IconLink.tsx index 4d6420d49b2..cde78973fd6 100644 --- a/apps/docs/features/ui/IconLink.tsx +++ b/apps/docs/features/ui/IconLink.tsx @@ -1,6 +1,6 @@ import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker' import Link from 'next/link' -import { type ReactNode } from 'react' +import { type ButtonHTMLAttributes, type ReactNode } from 'react' import { cn } from 'ui' const sizeStyles = { @@ -23,6 +23,33 @@ export type IconLinkItem = { className?: string } +const iconLinkClassName = + 'group relative -m-3 flex items-center gap-3 rounded-xl p-3 no-underline transition-colors hover:bg-accent focus-ring focus-visible:bg-accent' + +function IconLinkContent({ + title, + icon, + size = 'sm', +}: { + title: string + icon: ReactNode + size?: IconLinkSize +}) { + return ( + <> +
    + {icon} +
    + {title} + + ) +} + export function IconLink({ href, title, @@ -37,41 +64,62 @@ export function IconLink({ className?: string }) { return ( - -
    - {icon} -
    - {title} + + ) } +export function IconLinkButton({ + title, + icon, + size = 'sm', + className, + disabled, + tabIndex, + ...props +}: { + title: string + icon: ReactNode + size?: IconLinkSize + className?: string +} & ButtonHTMLAttributes) { + return ( + + ) +} + export function IconLinkList({ items, labelledBy, + label, className, itemClassName = 'col-span-6 md:col-span-4', size = 'sm', }: { items: IconLinkItem[] + /** Prefer when a visible heading is the list’s name. */ labelledBy?: string + /** Prefer when the nearby heading is section/setup copy, not a list title. */ + label?: string className?: string itemClassName?: string size?: IconLinkSize }) { return ( -