diff --git a/.agents/skills/ask-the-docs/reference/app-map.md b/.agents/skills/ask-the-docs/reference/app-map.md index bd50f01240a..009653bd6fc 100644 --- a/.agents/skills/ask-the-docs/reference/app-map.md +++ b/.agents/skills/ask-the-docs/reference/app-map.md @@ -53,8 +53,7 @@ flowchart TB | -------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `app/` | Next.js App Router — thin route files that delegate to feature modules | Slug-based catch-alls per section, e.g. `guides/auth/[[...slug]]/page.tsx` | | `apps/docs/content/guides/` | Source MDX for `/docs/guides/...` pages | One file per page; `_partials/` for shared blocks | -| `apps/docs/content/_partials/` | Reusable MDX snippets included via `<$Partial path="..." />` | Recursion supported | -| `apps/docs/content/troubleshooting/` | Troubleshooting articles | Some synced from GitHub issues via `Troubleshooting.script.mjs` | +| `apps/docs/content/_partials/` | Reusable MDX snippets included via `<$Partial path="..." />` | Recursion supported | | | `apps/docs/components/` | React components used inside MDX | One folder per component or component family | | `apps/docs/data/` | Typed data modules consumed by components | `.data.ts` suffix; lookup helpers live in `.utils.ts`, not here | | `apps/docs/lib/` | Pure library code shared across pipelines | Schemas (zod), helpers (`.utils.ts`), tests | @@ -218,14 +217,10 @@ Wraps the app with: - `DocsCommandProvider` / `DocsCommandMenu` - `SiteLayout` from `layouts/` -## Troubleshooting page subtree -- `features/docs/Troubleshooting.page.tsx` — entry-level page renderer. -- `features/docs/Troubleshooting.utils.ts` — TS utils. -- `features/docs/Troubleshooting.utils.common.mjs` — `.mjs` because it's - consumed by both the Next.js build _and_ a Node sync script with import - resolution quirks. **Don't convert to `.ts`** without checking the sync - script. +- `features/docs/Troubleshooting.utils.common.mjs` — `.mjs`, shared between + the Next.js build and other scripts that read troubleshooting entries + (e.g. search embeddings generation). - Topics enum in `TroubleshootingSchema` (the `topics` field) is the source of truth for product tag values. diff --git a/.agents/skills/ask-the-docs/reference/build-pipeline.md b/.agents/skills/ask-the-docs/reference/build-pipeline.md index 74f0318bb3f..1c79a09f2d1 100644 --- a/.agents/skills/ask-the-docs/reference/build-pipeline.md +++ b/.agents/skills/ask-the-docs/reference/build-pipeline.md @@ -140,8 +140,6 @@ pnpm dev # http://localhost:3001/docs ``` - **`predev`** runs GraphQL and reference codegen plus example copy. -- A concurrent watcher syncs troubleshooting content - (`dev:watch:troubleshooting`). - Community contributors: set `NEXT_PUBLIC_IS_PLATFORM=false` in `.env`. - Supabase employees: `pnpm run dev:secrets:pull` for internal env vars (AWS profile + `scripts/getSecrets.js`). diff --git a/.agents/skills/ask-the-docs/reference/gotchas.md b/.agents/skills/ask-the-docs/reference/gotchas.md index 54cbd296a64..ca659d7eee4 100644 --- a/.agents/skills/ask-the-docs/reference/gotchas.md +++ b/.agents/skills/ask-the-docs/reference/gotchas.md @@ -76,15 +76,6 @@ Specific traps to watch for. One-liner per item. URLs are passed through. Use it consistently in markdown output to avoid broken links. -## Troubleshooting subtree - -- `Troubleshooting.utils.common.mjs` is `.mjs` because the troubleshooting - sync script can't resolve `.ts` imports cleanly. Don't convert it. -- Rebasing can resurrect a **stale** version of this file when you reset - unrelated changes — verify against current `master` after - `git checkout master -- `. -- The `topics` enum in `TroubleshootingSchema` is hand-maintained. Adding a - new product means updating that enum. ## Build / CI diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e39729fd27f..2cc484c5ab8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,8 @@ /apps/docs/ @supabase/docs +/apps/kb/ @supabase/docs + /apps/www/ @supabase/marketing /apps/www/public/images/blog @supabase/marketing /apps/www/lib/redirects.js diff --git a/.github/workflows/docs-sync-troubleshooting.yml b/.github/workflows/docs-sync-troubleshooting.yml deleted file mode 100644 index 4b671232b4e..00000000000 --- a/.github/workflows/docs-sync-troubleshooting.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: '[Docs] Sync troubleshooting guides to GitHub Discussions' - -on: - push: - branches: - - master - paths: - - 'apps/docs/content/troubleshooting/**' - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - update-troubleshooting: - runs-on: blacksmith-4vcpu-ubuntu-2404 - - env: - DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }} - DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }} - DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }} - NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }} - SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }} - - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - sparse-checkout: | - apps/docs - patches - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install pnpm - with: - run_install: false - - - name: Use Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version-file: '.nvmrc' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Run Troubleshooting script - run: pnpm run --filter docs troubleshooting:sync - - - name: Create Pull Request - uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5 - with: - commit-message: '[bot] sync troubleshooting guides to db' - title: '[bot] sync troubleshooting guides to db' - author: 'github-docs-sync-bot ' - branch: 'bot/docs-sync-troubleshooting' - branch-suffix: 'random' - labels: 'documentation' diff --git a/.github/workflows/kb-troubleshooting-sync.yml b/.github/workflows/kb-troubleshooting-sync.yml new file mode 100644 index 00000000000..9103d2278b4 --- /dev/null +++ b/.github/workflows/kb-troubleshooting-sync.yml @@ -0,0 +1,56 @@ +name: 'KB Troubleshooting Sync' + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + +jobs: + sync: + runs-on: blacksmith-4vcpu-ubuntu-2404 + + env: + KB_GITHUB_APP_ID: ${{ secrets.KB_GITHUB_APP_ID }} + KB_GITHUB_APP_INSTALLATION_ID: ${{ secrets.KB_GITHUB_APP_INSTALLATION_ID }} + KB_GITHUB_APP_PRIVATE_KEY: ${{ secrets.KB_GITHUB_APP_PRIVATE_KEY }} + SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }} + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + sparse-checkout: | + apps/kb + packages + patches + + - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + name: Install pnpm + with: + run_install: false + + - name: Use Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: '.nvmrc' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Backfill legacy troubleshooting slugs + run: pnpm run --filter kb sync:legacy-slugs + + - name: Fetch federated troubleshooting content + run: pnpm run --filter kb fetch:federated-content + + - name: Sync new troubleshooting entries + run: pnpm run --filter kb sync:troubleshooting-entries + + - name: Sync troubleshooting entry updates + run: pnpm run --filter kb sync:troubleshooting-updates diff --git a/apps/design-system/app/favicon.ico b/apps/design-system/app/favicon.ico deleted file mode 100644 index 718d6fea483..00000000000 Binary files a/apps/design-system/app/favicon.ico and /dev/null differ diff --git a/apps/design-system/app/layout.tsx b/apps/design-system/app/layout.tsx index 31c70dfced9..5f0a568bd68 100644 --- a/apps/design-system/app/layout.tsx +++ b/apps/design-system/app/layout.tsx @@ -11,11 +11,14 @@ import { inter, manrope, sourceCodePro } from '@/lib/fonts' const className = `${inter.variable} ${manrope.variable} ${sourceCodePro.variable}` +const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon' + export const metadata: Metadata = { applicationName: 'Supabase Design System', title: 'Supabase Design System', description: 'Design resources for building consistent user experiences at Supabase.', - icons: genFaviconData(BASE_PATH), + icons: genFaviconData(BASE_PATH, FAVICON_ROUTE), + manifest: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json`, } export const viewport: Viewport = { diff --git a/apps/design-system/components/component-example.tsx b/apps/design-system/components/component-example.tsx index 8447963453b..341d5dcfa9a 100644 --- a/apps/design-system/components/component-example.tsx +++ b/apps/design-system/components/component-example.tsx @@ -26,11 +26,11 @@ export function ComponentExample({ }: ComponentExampleProps) { const [Example, Code, ...Children] = React.Children.toArray(children) as React.ReactElement<{ children: React.ReactNode - 'data-rehype-pretty-code-fragment'?: unknown + 'data-rehype-pretty-code-figure'?: unknown }>[] const codeString = React.useMemo(() => { - if (typeof Code?.props['data-rehype-pretty-code-fragment'] !== 'undefined') { + if (typeof Code?.props['data-rehype-pretty-code-figure'] !== 'undefined') { const [, Button] = React.Children.toArray(Code.props.children) as React.ReactElement<{ value?: string __rawString__?: string diff --git a/apps/design-system/components/source-panel.tsx b/apps/design-system/components/source-panel.tsx index 05409f8163c..e0cdc0d6466 100644 --- a/apps/design-system/components/source-panel.tsx +++ b/apps/design-system/components/source-panel.tsx @@ -310,8 +310,21 @@ const SourcePanel = forwardRef & } } + const hasSource = Boolean( + doc.source?.radix || + doc.source?.shadcn || + doc.source?.vaul || + doc.source?.inputOtp || + doc.source?.reactAccessibleTreeview || + doc.source?.recharts + ) + + if (!hasSource) { + return null + } + return ( -
+
diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index 4ad98ae7c13..ee67195a54a 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -127,11 +127,6 @@ export const docsConfig: DocsConfig = { href: '/docs/fragments/admonition', items: [], }, - { - title: 'Collapsible Alert', - href: '/docs/fragments/collapsible-alert', - items: [], - }, { title: 'Collapsible Card Section', href: '/docs/fragments/collapsible-card-section', diff --git a/apps/design-system/content/docs/components/alert.mdx b/apps/design-system/content/docs/components/alert.mdx index f688e7e79c4..d522e8c45fa 100644 --- a/apps/design-system/content/docs/components/alert.mdx +++ b/apps/design-system/content/docs/components/alert.mdx @@ -11,7 +11,7 @@ Alert is the low-level shadcn primitive that forms the foundation for [Admonitio Examples where Alert may be appropriate include bespoke icons, custom internal layouts, or wrapper components that need direct access to `alertVariants`. -Use [Collapsible Alert](../fragments/collapsible-alert) when a callout also needs expandable detail. Use [Collapsible](./collapsible) only for generic disclosure behaviour that is not itself an alert. +Compose Alert with [Collapsible](./collapsible) when a callout needs expandable detail. Use [DialogDisclosure](./dialog#optional-details) for optional details in a dialog. diff --git a/apps/design-system/content/docs/components/collapsible.mdx b/apps/design-system/content/docs/components/collapsible.mdx index 3b6b31bd1ff..bbe61f9ddb2 100644 --- a/apps/design-system/content/docs/components/collapsible.mdx +++ b/apps/design-system/content/docs/components/collapsible.mdx @@ -6,7 +6,7 @@ component: true Collapsible is a low-level primitive for revealing and hiding related content. Use it when a component needs custom trigger or content composition. -Use Collapsible for generic disclosure behaviour: rows, sections, menus, tool details, and custom layouts. Use [Collapsible Alert](../fragments/collapsible-alert) instead when the disclosure is an alert-styled callout that should keep standard alert padding, variants, trigger styling, and toggle treatment. +Use Collapsible for generic disclosure behavior: rows, sections, menus, tool details, and custom layouts. Use [DialogDisclosure](./dialog#optional-details) for optional details in a dialog. diff --git a/apps/design-system/content/docs/components/dialog.mdx b/apps/design-system/content/docs/components/dialog.mdx index 3dffe213230..e95002e1b28 100644 --- a/apps/design-system/content/docs/components/dialog.mdx +++ b/apps/design-system/content/docs/components/dialog.mdx @@ -65,6 +65,14 @@ import { ## Examples +### Optional details + +Use `DialogDisclosure` to reveal optional help or context inside a dialog. Put the trigger in the dialog body above the footer, and add a section separator when the disclosure follows other content. Keep the primary action visible while the details are open or closed. + +`Collapsible` provides the open and closed state. `DialogDisclosure` adds the trigger and content presentation used in dialogs. Use `Accordion` when several related sections each need a heading and expandable content. + + + ### Custom close button diff --git a/apps/design-system/content/docs/fragments/admonition.mdx b/apps/design-system/content/docs/fragments/admonition.mdx index 8e5752ca4b2..087ca0ed3bd 100644 --- a/apps/design-system/content/docs/fragments/admonition.mdx +++ b/apps/design-system/content/docs/fragments/admonition.mdx @@ -15,7 +15,7 @@ Admonition provides the standard product callout pattern for situations that req Use Admonition instead of the low-level [Alert](../components/alert) primitive unless you specifically need custom composition. If in doubt, stick with Admonition. -Use [Collapsible Alert](./collapsible-alert) when the callout needs to remain visible while optional detail can be expanded. Use [Collapsible](../components/collapsible) for generic disclosure behaviour that is not itself a callout. +Use [Collapsible](../components/collapsible) for custom expandable content. Use [DialogDisclosure](../components/dialog#optional-details) for optional details in a dialog. Use `title` for the heading slot when the callout needs its own heading. `title` is optional: short callouts may use `description` without a title when nearby content already provides enough context. diff --git a/apps/design-system/content/docs/fragments/collapsible-alert.mdx b/apps/design-system/content/docs/fragments/collapsible-alert.mdx deleted file mode 100644 index 5cae9a4934f..00000000000 --- a/apps/design-system/content/docs/fragments/collapsible-alert.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Collapsible Alert -description: An alert-styled block with a collapsible content section, toggled by an icon button. -fragment: true ---- - -Collapsible Alert combines an Alert-style container with a collapsible content section. Use it when the callout itself needs to stay visible while optional detail can be expanded. - -Use Collapsible Alert when the disclosure is itself a callout. It owns the alert styling, padding, trigger treatment, and toggle button. Use [Collapsible](../components/collapsible) instead when you only need generic reveal and hide behaviour and the surrounding layout is not an alert. - -For non-expandable product callouts, use [Admonition](./admonition). Use the low-level [Alert](../components/alert) primitive only when a wrapper or bespoke composition needs direct control over the alert structure. - - - -## Usage - -```tsx -import { CollapsibleAlert } from 'ui-patterns/collapsible-alert' -``` - -```tsx - -

Content revealed when expanded.

-
-``` - -### Default open - -```tsx - -

This section is expanded by default.

-
-``` - -### Variants - -Supports the same `variant` prop as Alert: `default`, `destructive`, and `warning`. - -```tsx - -

Something needs attention.

-
-``` diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 438b1c3ce3a..eb7d651917c 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -40,7 +40,7 @@ "react-wrap-balancer": "^1.1.0", "recharts": "catalog:", "rehype-autolink-headings": "^7.1.0", - "rehype-pretty-code": "^0.9.0", + "rehype-pretty-code": "catalog:", "rehype-slug": "^6.0.0", "remark": "^14.0.3", "remark-code-import": "^1.2.0", @@ -54,7 +54,6 @@ "zod": "catalog:" }, "devDependencies": { - "@shikijs/compat": "^1.1.7", "@types/lodash.template": "4.5.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", @@ -64,7 +63,7 @@ "npm-run-all": "^4.1.5", "postcss": "catalog:", "rimraf": "^4.1.3", - "shiki": "^1.1.7", + "shiki": "catalog:", "tailwindcss": "catalog:", "tsconfig": "workspace:*", "tsx": "catalog:", diff --git a/apps/design-system/public/favicon/android-icon-144x144.png b/apps/design-system/public/favicon/android-icon-144x144.png deleted file mode 100644 index 0b375991a84..00000000000 Binary files a/apps/design-system/public/favicon/android-icon-144x144.png and /dev/null differ diff --git a/apps/design-system/public/favicon/android-icon-192x192.png b/apps/design-system/public/favicon/android-icon-192x192.png index d84f0567505..f33fbfca361 100644 Binary files a/apps/design-system/public/favicon/android-icon-192x192.png and b/apps/design-system/public/favicon/android-icon-192x192.png differ diff --git a/apps/design-system/public/favicon/android-icon-36x36.png b/apps/design-system/public/favicon/android-icon-36x36.png deleted file mode 100644 index 35ad4f1c585..00000000000 Binary files a/apps/design-system/public/favicon/android-icon-36x36.png and /dev/null differ diff --git a/apps/design-system/public/favicon/android-icon-48x48.png b/apps/design-system/public/favicon/android-icon-48x48.png deleted file mode 100644 index 9aa3e5e8a83..00000000000 Binary files a/apps/design-system/public/favicon/android-icon-48x48.png and /dev/null differ diff --git a/apps/design-system/public/favicon/android-icon-512x512.png b/apps/design-system/public/favicon/android-icon-512x512.png new file mode 100644 index 00000000000..d861a381b0f Binary files /dev/null and b/apps/design-system/public/favicon/android-icon-512x512.png differ diff --git a/apps/design-system/public/favicon/android-icon-72x72.png b/apps/design-system/public/favicon/android-icon-72x72.png deleted file mode 100644 index 99ef50933b2..00000000000 Binary files a/apps/design-system/public/favicon/android-icon-72x72.png and /dev/null differ diff --git a/apps/design-system/public/favicon/android-icon-96x96.png b/apps/design-system/public/favicon/android-icon-96x96.png deleted file mode 100644 index e3180284847..00000000000 Binary files a/apps/design-system/public/favicon/android-icon-96x96.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-114x114.png b/apps/design-system/public/favicon/apple-icon-114x114.png deleted file mode 100644 index 92fe03ae5dc..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-114x114.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-120x120.png b/apps/design-system/public/favicon/apple-icon-120x120.png deleted file mode 100644 index 3274a504b87..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-120x120.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-144x144.png b/apps/design-system/public/favicon/apple-icon-144x144.png deleted file mode 100644 index 0b375991a84..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-144x144.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-152x152.png b/apps/design-system/public/favicon/apple-icon-152x152.png deleted file mode 100644 index fed0b817e8c..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-152x152.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-180x180.png b/apps/design-system/public/favicon/apple-icon-180x180.png index 456f3c7dd9d..af01e55deac 100644 Binary files a/apps/design-system/public/favicon/apple-icon-180x180.png and b/apps/design-system/public/favicon/apple-icon-180x180.png differ diff --git a/apps/design-system/public/favicon/apple-icon-57x57.png b/apps/design-system/public/favicon/apple-icon-57x57.png deleted file mode 100644 index e8f2032bff5..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-57x57.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-60x60.png b/apps/design-system/public/favicon/apple-icon-60x60.png deleted file mode 100644 index 7619397e3e3..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-60x60.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-72x72.png b/apps/design-system/public/favicon/apple-icon-72x72.png deleted file mode 100644 index 99ef50933b2..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-72x72.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-76x76.png b/apps/design-system/public/favicon/apple-icon-76x76.png deleted file mode 100644 index 83af926e5d5..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-76x76.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon-precomposed.png b/apps/design-system/public/favicon/apple-icon-precomposed.png deleted file mode 100644 index d84f0567505..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon-precomposed.png and /dev/null differ diff --git a/apps/design-system/public/favicon/apple-icon.png b/apps/design-system/public/favicon/apple-icon.png deleted file mode 100644 index d84f0567505..00000000000 Binary files a/apps/design-system/public/favicon/apple-icon.png and /dev/null differ diff --git a/apps/design-system/public/favicon/browserconfig.xml b/apps/design-system/public/favicon/browserconfig.xml deleted file mode 100644 index 1a8136def88..00000000000 --- a/apps/design-system/public/favicon/browserconfig.xml +++ /dev/null @@ -1,2 +0,0 @@ - -#ffffff \ No newline at end of file diff --git a/apps/design-system/public/favicon/favicon-128.png b/apps/design-system/public/favicon/favicon-128.png deleted file mode 100644 index 2482e4433b3..00000000000 Binary files a/apps/design-system/public/favicon/favicon-128.png and /dev/null differ diff --git a/apps/design-system/public/favicon/favicon-16x16.png b/apps/design-system/public/favicon/favicon-16x16.png deleted file mode 100644 index b65de8d0cd3..00000000000 Binary files a/apps/design-system/public/favicon/favicon-16x16.png and /dev/null differ diff --git a/apps/design-system/public/favicon/favicon-180x180.png b/apps/design-system/public/favicon/favicon-180x180.png deleted file mode 100644 index 456f3c7dd9d..00000000000 Binary files a/apps/design-system/public/favicon/favicon-180x180.png and /dev/null differ diff --git a/apps/design-system/public/favicon/favicon-196x196.png b/apps/design-system/public/favicon/favicon-196x196.png deleted file mode 100644 index a8c2c723f79..00000000000 Binary files a/apps/design-system/public/favicon/favicon-196x196.png and /dev/null differ diff --git a/apps/design-system/public/favicon/favicon-32x32.png b/apps/design-system/public/favicon/favicon-32x32.png deleted file mode 100644 index 737dee885ea..00000000000 Binary files a/apps/design-system/public/favicon/favicon-32x32.png and /dev/null differ diff --git a/apps/design-system/public/favicon/favicon-48x48.png b/apps/design-system/public/favicon/favicon-48x48.png deleted file mode 100644 index eb6a7ec729a..00000000000 Binary files a/apps/design-system/public/favicon/favicon-48x48.png and /dev/null differ diff --git a/apps/design-system/public/favicon/favicon-96x96.png b/apps/design-system/public/favicon/favicon-96x96.png deleted file mode 100644 index e3180284847..00000000000 Binary files a/apps/design-system/public/favicon/favicon-96x96.png and /dev/null differ diff --git a/apps/design-system/public/favicon/favicon.ico b/apps/design-system/public/favicon/favicon.ico index 49c20b9d6ec..4a970cd4c46 100644 Binary files a/apps/design-system/public/favicon/favicon.ico and b/apps/design-system/public/favicon/favicon.ico differ diff --git a/apps/design-system/public/favicon/local/android-icon-192x192.png b/apps/design-system/public/favicon/local/android-icon-192x192.png new file mode 100644 index 00000000000..a174cf0468a Binary files /dev/null and b/apps/design-system/public/favicon/local/android-icon-192x192.png differ diff --git a/apps/design-system/public/favicon/local/android-icon-512x512.png b/apps/design-system/public/favicon/local/android-icon-512x512.png new file mode 100644 index 00000000000..a5fc7d6b949 Binary files /dev/null and b/apps/design-system/public/favicon/local/android-icon-512x512.png differ diff --git a/apps/design-system/public/favicon/local/apple-icon-180x180.png b/apps/design-system/public/favicon/local/apple-icon-180x180.png new file mode 100644 index 00000000000..775ce59ec9b Binary files /dev/null and b/apps/design-system/public/favicon/local/apple-icon-180x180.png differ diff --git a/apps/design-system/public/favicon/local/favicon.ico b/apps/design-system/public/favicon/local/favicon.ico new file mode 100644 index 00000000000..7cc4cd192a9 Binary files /dev/null and b/apps/design-system/public/favicon/local/favicon.ico differ diff --git a/apps/design-system/public/favicon/local/manifest.json b/apps/design-system/public/favicon/local/manifest.json new file mode 100644 index 00000000000..1d6763c029d --- /dev/null +++ b/apps/design-system/public/favicon/local/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Supabase Design System", + "short_name": "Supabase Design System", + "description": "Design resources for building consistent user experiences at Supabase.", + "display": "standalone", + "theme_color": "#1C1C1C", + "background_color": "#1C1C1C", + "icons": [ + { + "src": "android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/apps/design-system/public/favicon/manifest.json b/apps/design-system/public/favicon/manifest.json index 9da79d27be6..1d6763c029d 100644 --- a/apps/design-system/public/favicon/manifest.json +++ b/apps/design-system/public/favicon/manifest.json @@ -7,40 +7,16 @@ "background_color": "#1C1C1C", "icons": [ { - "src": "/favicon/android-icon-36x36.png", - "sizes": "36x36", - "type": "image/png", - "density": "0.75" - }, - { - "src": "/favicon/android-icon-48x48.png", - "sizes": "48x48", - "type": "image/png", - "density": "1.0" - }, - { - "src": "/favicon/android-icon-72x72.png", - "sizes": "72x72", - "type": "image/png", - "density": "1.5" - }, - { - "src": "/favicon/android-icon-96x96.png", - "sizes": "96x96", - "type": "image/png", - "density": "2.0" - }, - { - "src": "/favicon/android-icon-144x144.png", - "sizes": "144x144", - "type": "image/png", - "density": "3.0" - }, - { - "src": "/favicon/android-icon-192x192.png", + "src": "android-icon-192x192.png", "sizes": "192x192", "type": "image/png", - "density": "4.0" + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" } ] } diff --git a/apps/design-system/public/favicon/ms-icon-144x144.png b/apps/design-system/public/favicon/ms-icon-144x144.png deleted file mode 100644 index 0b375991a84..00000000000 Binary files a/apps/design-system/public/favicon/ms-icon-144x144.png and /dev/null differ diff --git a/apps/design-system/public/favicon/ms-icon-150x150.png b/apps/design-system/public/favicon/ms-icon-150x150.png deleted file mode 100644 index a87e8ae2378..00000000000 Binary files a/apps/design-system/public/favicon/ms-icon-150x150.png and /dev/null differ diff --git a/apps/design-system/public/favicon/ms-icon-310x310.png b/apps/design-system/public/favicon/ms-icon-310x310.png deleted file mode 100644 index 6cb63ac25b0..00000000000 Binary files a/apps/design-system/public/favicon/ms-icon-310x310.png and /dev/null differ diff --git a/apps/design-system/public/favicon/ms-icon-70x70.png b/apps/design-system/public/favicon/ms-icon-70x70.png deleted file mode 100644 index 5d6b8e395c2..00000000000 Binary files a/apps/design-system/public/favicon/ms-icon-70x70.png and /dev/null differ diff --git a/apps/design-system/registry/default/example/collapsible-alert-demo.tsx b/apps/design-system/registry/default/example/collapsible-alert-demo.tsx deleted file mode 100644 index 96cb3e30324..00000000000 --- a/apps/design-system/registry/default/example/collapsible-alert-demo.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { CollapsibleAlert } from 'ui-patterns/collapsible-alert' - -export default function CollapsibleAlertDemo() { - return ( - -

- Try a different browser or disable extensions that block network requests. -

-
- ) -} diff --git a/apps/design-system/registry/default/example/dialog-disclosure-demo.tsx b/apps/design-system/registry/default/example/dialog-disclosure-demo.tsx new file mode 100644 index 00000000000..a69bd06fbed --- /dev/null +++ b/apps/design-system/registry/default/example/dialog-disclosure-demo.tsx @@ -0,0 +1,64 @@ +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, +} from 'ui' +import { + DialogDisclosure, + DialogDisclosureContent, + DialogDisclosureTrigger, +} from 'ui-patterns/DialogDisclosure' + +export default function DialogDisclosureDemo() { + return ( + + + + + + + Export results + + + +

+ All 250 rows will be downloaded as a CSV file. +

+
+ + + + What’s included + + + +

The file includes column names in the first row, followed by the query results.

+
    +
  • Columns keep the same order as the results table.
  • +
  • Empty fields represent null values.
  • +
  • Values containing commas or line breaks are enclosed in double quotes.
  • +
+

Open the file in a spreadsheet or import it into another tool.

+
+
+
+ + + + + + + + +
+
+ ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index dc2f26e11be..39a2d0bb327 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -67,12 +67,6 @@ export const examples: Registry = [ registryDependencies: ['alert'], files: ['example/alert-demo.tsx'], }, - { - name: 'collapsible-alert-demo', - type: 'components:example', - registryDependencies: ['collapsible-alert'], - files: ['example/collapsible-alert-demo.tsx'], - }, { name: 'collapsible-card-section-demo', type: 'components:example', @@ -551,6 +545,12 @@ export const examples: Registry = [ registryDependencies: ['dialog'], files: ['example/dialog-demo.tsx'], }, + { + name: 'dialog-disclosure-demo', + type: 'components:example', + registryDependencies: ['dialog', 'collapsible', 'button'], + files: ['example/dialog-disclosure-demo.tsx'], + }, { name: 'dialog-close-button', type: 'components:example', diff --git a/apps/design-system/styles/mdx.css b/apps/design-system/styles/mdx.css index 32f049edca0..2b3ed69ff0e 100644 --- a/apps/design-system/styles/mdx.css +++ b/apps/design-system/styles/mdx.css @@ -16,25 +16,25 @@ display: block; } -[data-rehype-pretty-code-fragment] { +[data-rehype-pretty-code-figure] { @apply relative; } -[data-rehype-pretty-code-fragment] code { +[data-rehype-pretty-code-figure] code { @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0; counter-reset: line; box-decoration-break: clone; } -[data-rehype-pretty-code-fragment] .line { +[data-rehype-pretty-code-figure] [data-line] { @apply px-4 min-h-[1rem] py-0.5 w-full inline-block; } -[data-rehype-pretty-code-fragment] [data-line-numbers] .line { +[data-rehype-pretty-code-figure] [data-line-numbers] [data-line] { @apply px-2; } -[data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before { +[data-rehype-pretty-code-figure] [data-line-numbers] > [data-line]::before { @apply text-foreground-muted text-xs; counter-increment: line; content: counter(line); @@ -44,23 +44,24 @@ text-align: right; } -[data-rehype-pretty-code-fragment] .line--highlighted { +[data-rehype-pretty-code-figure] .line--highlighted { @apply relative bg-surface-200 dark:bg-surface-100; } -[data-rehype-pretty-code-fragment] .line--highlighted { +[data-rehype-pretty-code-figure] .line--highlighted { @apply border-foreground-muted border-l-2; } -[data-rehype-pretty-code-fragment] .line-highlighted span { +[data-rehype-pretty-code-figure] .line-highlighted span { @apply relative; } -[data-rehype-pretty-code-fragment] .word--highlighted { - @apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1; +/* reset default text color */ +[data-rehype-pretty-code-figure] .word--highlighted { + @apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1 text-inherit; } -.dark [data-rehype-pretty-code-fragment] .word--highlighted { +.dark [data-rehype-pretty-code-figure] .word--highlighted { @apply bg-zinc-900; } diff --git a/apps/design-system/turbo.jsonc b/apps/design-system/turbo.jsonc index b61bfcc7955..29bec7c187d 100644 --- a/apps/design-system/turbo.jsonc +++ b/apps/design-system/turbo.jsonc @@ -4,7 +4,7 @@ "tasks": { "build": { "dependsOn": ["^build"], - "env": ["NEXT_PUBLIC_BASE_PATH", "NEXT_PUBLIC_APP_URL"], + "env": ["NODE_ENV", "NEXT_PUBLIC_BASE_PATH", "NEXT_PUBLIC_APP_URL"], "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**/*", ".velite/**"], }, }, diff --git a/apps/design-system/velite.config.js b/apps/design-system/velite.config.js index f6805e6a44d..0d6f02c447b 100644 --- a/apps/design-system/velite.config.js +++ b/apps/design-system/velite.config.js @@ -1,7 +1,6 @@ /* eslint-disable turbo/no-undeclared-env-vars */ -import { mkdir, rename, writeFile } from 'node:fs/promises' +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises' import path from 'path' -import { getHighlighter, loadTheme } from '@shikijs/compat' import rehypeAutolinkHeadings from 'rehype-autolink-headings' import rehypePrettyCode from 'rehype-pretty-code' import rehypeSlug from 'rehype-slug' @@ -14,6 +13,10 @@ import { rehypeComponent } from './lib/rehype-component' const CODE_OUTPUT_DIR = '.velite/codes' +const codeTheme = JSON.parse( + await readFile(path.join(process.cwd(), 'lib/themes/supabase-2.json'), 'utf8') +) + function toCodeId(slugAsParams) { if (!slugAsParams) return 'index' return Buffer.from(slugAsParams, 'utf8').toString('base64url') @@ -118,18 +121,9 @@ export default defineConfig({ [ rehypePrettyCode, { - // Memoized so the (expensive) theme parse + oniguruma WASM init runs once - // for the whole build, instead of once per file — velite compiles every - // file concurrently, so without this every doc pays that cost redundantly. - getHighlighter: (() => { - let highlighterPromise - return () => { - highlighterPromise ??= loadTheme( - path.join(process.cwd(), '/lib/themes/supabase-2.json') - ).then((theme) => getHighlighter({ theme })) - return highlighterPromise - } - })(), + // plugin shares one cached highlighter per theme, lazy language loading needs shiki >= 4.5 (shikijs/shiki#1318) + theme: codeTheme, + keepBackground: false, onVisitLine(node) { // Prevent lines from collapsing in `display: grid` mode, and allow empty // lines to be copy/pasted @@ -138,17 +132,18 @@ export default defineConfig({ } }, onVisitHighlightedLine(node) { - node.properties.className.push('line--highlighted') + // the plugin strips the `line` class first, leaving className undefined + node.properties.className = [...(node.properties.className ?? []), 'line--highlighted'] }, - onVisitHighlightedWord(node) { + onVisitHighlightedChars(node) { node.properties.className = ['word--highlighted'] }, }, ], () => (tree) => { visit(tree, (node) => { - if (node?.type === 'element' && node?.tagName === 'div') { - if (!('data-rehype-pretty-code-fragment' in node.properties)) { + if (node?.type === 'element' && node?.tagName === 'figure') { + if (!('data-rehype-pretty-code-figure' in node.properties)) { return } @@ -157,7 +152,7 @@ export default defineConfig({ return } - preElement.properties['__withMeta__'] = node.children.at(0).tagName === 'div' + preElement.properties['__withMeta__'] = node.children.at(0).tagName === 'figcaption' preElement.properties['__rawString__'] = node.__rawString__ if (node.__src__) { diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 74d9f33e80d..f96557fa3ff 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -19,13 +19,16 @@ const { metadataApplicationName, metadataTitle } = getCustomContent([ 'metadata:title', ]) +const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon' + const metadata: Metadata = { applicationName: metadataApplicationName, title: metadataTitle, description: 'Supabase is the Postgres development platform providing all the backend features you need to build a product.', metadataBase: new URL('https://supabase.com'), - icons: genFaviconData(BASE_PATH), + icons: genFaviconData(BASE_PATH, FAVICON_ROUTE), + manifest: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json`, robots: { index: IS_PRODUCTION, follow: IS_PRODUCTION, diff --git a/apps/docs/components/DocsCoverLogo.tsx b/apps/docs/components/DocsCoverLogo.tsx index eebc87c0092..8ae457c4d49 100644 --- a/apps/docs/components/DocsCoverLogo.tsx +++ b/apps/docs/components/DocsCoverLogo.tsx @@ -1,6 +1,6 @@ 'use client' -import { domAnimation, LazyMotion, m } from 'framer-motion' +import { domAnimation, LazyMotion, m } from 'motion/react' import React, { type ComponentPropsWithoutRef } from 'react' const DocsCoverLogo = ({ className, ...props }: ComponentPropsWithoutRef<'div'>) => { diff --git a/apps/docs/components/Navigation/Navigation.commands.tsx b/apps/docs/components/Navigation/Navigation.commands.tsx index 9a9c6c92f18..8ee7409aa98 100644 --- a/apps/docs/components/Navigation/Navigation.commands.tsx +++ b/apps/docs/components/Navigation/Navigation.commands.tsx @@ -62,7 +62,7 @@ const navCommands = [ { id: 'nav-local-cli', name: 'Go to Local Dev / CLI', - route: '/guides/cli', + route: '/guides/local-development', icon: () => , }, { diff --git a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx index 903373a09b3..4ec6d344e26 100644 --- a/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/GlobalMobileMenu.tsx @@ -1,7 +1,7 @@ import { getCustomContent } from '~/lib/custom-content/getCustomContent' import { useIsLoggedIn, useIsUserLoading } from 'common' -import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion' import { X } from 'lucide-react' +import { AnimatePresence, domAnimation, LazyMotion, m } from 'motion/react' import Image from 'next/image' import Link from 'next/link' import { usePathname } from 'next/navigation' diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index f20b9443ae4..61ee0ae53bc 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -259,7 +259,6 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [ icon: 'reference-csharp', href: '/reference/csharp' as `/${string}`, level: 'reference_csharp', - community: true, enabled: sdkCsharpEnabled, }, { @@ -541,18 +540,6 @@ export const gettingstarted: NavMenuConstant = { ], } -export const cli = { - title: 'CLI', - items: [ - { name: 'Overview', url: '/guides/cli' }, - { name: 'Managing Environments', url: '/guides/cli/managing-environments' }, - { - name: 'Using environment variables in config.toml', - url: '/guides/cli/using-environment-variables-in-config', - }, - ], -} - export const NativeMobileLoginItems = [ { name: 'Apple', @@ -884,6 +871,7 @@ export const auth: NavMenuConstant = { { name: 'Auth0', url: '/guides/auth/third-party/auth0' }, { name: 'AWS Cognito (Amplify)', url: '/guides/auth/third-party/aws-cognito' }, { name: 'WorkOS', url: '/guides/auth/third-party/workos' }, + { name: 'Custom OIDC', url: '/guides/auth/third-party/custom-oidc' }, ], }, { @@ -2693,6 +2681,7 @@ export const security: NavMenuConstant = { url: undefined, items: [ { name: 'Platform configuration', url: '/guides/security/platform-security' }, + { name: 'TLS for project APIs', url: '/guides/security/tls' }, { name: 'Product configuration', url: '/guides/security/product-security' }, { name: 'Security testing', url: '/guides/security/security-testing' }, { name: 'Platform Audit Logs', url: '/guides/security/platform-audit-logs' }, @@ -3536,7 +3525,7 @@ export const reference_csharp_v0 = { parent: '/reference', pkg: { name: 'supabase', - repo: 'https://github.com/supabase-community/supabase-csharp', + repo: 'https://github.com/supabase/supabase-csharp', }, } @@ -3547,7 +3536,7 @@ export const reference_csharp_v1 = { parent: '/reference', pkg: { name: 'supabase', - repo: 'https://github.com/supabase-community/supabase-csharp', + repo: 'https://github.com/supabase/supabase-csharp', }, } @@ -3558,7 +3547,7 @@ export const reference_csharp_v8 = { parent: '/reference', pkg: { name: 'supabase', - repo: 'https://github.com/supabase-community/supabase-csharp', + repo: 'https://github.com/supabase/supabase-csharp', }, } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index 21a81eff18f..4e683aee723 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -13,6 +13,7 @@ import React, { useEffect, useRef } from 'react' import MenuIconPicker from './MenuIconPicker' type NavAccordionItem = { + name?: string url?: string enabled?: boolean items?: NavAccordionItem[] @@ -31,6 +32,15 @@ function isRenderable(item: NavAccordionItem): boolean { return item.items?.some(isRenderable) ?? false } +function groupLinks(item: NavAccordionItem): Array<{ name: string; url: string }> { + return (item.items ?? []) + .filter((child) => isRenderable(child)) + .flatMap((child) => [ + ...(child.url && child.name ? [{ name: child.name, url: child.url }] : []), + ...groupLinks(child), + ]) +} + const HeaderLink = React.memo(function HeaderLink(props: { title: string id: string @@ -164,6 +174,13 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any + ) : ( diff --git a/apps/docs/content/_partials/api_rate_limits.mdx b/apps/docs/content/_partials/api_rate_limits.mdx index c050a467f80..5bf2ea8e928 100644 --- a/apps/docs/content/_partials/api_rate_limits.mdx +++ b/apps/docs/content/_partials/api_rate_limits.mdx @@ -4,20 +4,39 @@ Rate limits are applied to prevent abuse and ensure fair usage of the Management ### Standard rate limit -| Limit | Duration | Scope | -| ------------ | -------- | ---------------------------------- | -| 120 requests | 1 minute | Per user, per project/organization | +| Limit | Duration | Scope | +| ------------ | -------- | --------------------------------------------------- | +| 120 requests | 1 minute | Per user, per project or organization, per endpoint | -When you exceed this rate limit, all subsequent API calls will return a `429 Too Many Requests` response for the remainder of the minute. Once the time window expires, your request quota resets and you can make requests again. +When you exceed this rate limit, all further API calls return a `429 Too Many Requests` response for the remainder of the minute. Once the time window expires, your request quota resets and you can make requests again. ### Rate limit scope -Rate limits are applied with per-user + per-scope isolation: +Rate limits are applied with per-user, per-scope, per-endpoint isolation: -- **Project scope**: Rate limits apply independently to each project. Requests to one project do not count toward the limit of another project. -- **Organization scope**: Rate limits apply independently to each organization. Requests to one organization do not count toward the limit of another organization. +- **Project scope**: rate limits apply independently to each project. +- **Organization scope**: rate limits apply independently to each organization. +- **User scope**: requests that don't include a project or organization, such as `GET /v1/projects` and `GET /v1/organizations`, are counted against the user. +- **Endpoint**: within each scope, every endpoint has its own limit. -This means you can make 120 requests to Project A and 120 requests to Project B within the same minute without hitting rate limits, as they are tracked separately. +For example, you can make 120 requests to Project A and 120 requests to Project B within the same minute, as they are tracked separately. Within one organization, calling `GET /v1/organizations/{slug}/members` 120 times doesn't affect calls to `GET /v1/organizations/{slug}` or `POST /v1/projects` for that organization. + +`POST /v1/projects` is organization-scoped, using the `organization_slug` in the request body. + +### How rate limits are tracked + +Your requests are identified and tracked using one of the following identifiers, in this order of priority: + +1. **OAuth App ID**: If your request is authenticated via an OAuth application +2. **User ID**: If your request is authenticated with a personal access token +3. **IP Address**: If your request is unauthenticated (extracted from request headers) + +Each identifier is combined with the scope (project or organization) and the endpoint to create a unique tracking key. This ensures that rate limits are isolated per user and per scope, preventing one project or organization from affecting another. + +### Who the limit applies to + +- **Personal access tokens**: the limit applies to the user, so all tokens created by the same user share it. +- **OAuth apps**: the limit applies to the app, shared by everyone calling through it. ### Rate limit response headers @@ -29,16 +48,6 @@ Every API response includes rate limit information following official [HTTP spec You can use these headers to monitor your usage and implement proactive rate limit handling before receiving a 429 response. -### How rate limits are tracked - -Your requests are identified and tracked using one of the following identifiers, in this order of priority: - -1. **OAuth App ID** - If your request is authenticated via an OAuth application -2. **User ID** - If your request is authenticated with a personal access token -3. **IP Address** - If your request is unauthenticated (extracted from request headers) - -Each identifier is combined with the scope (project or organization) to create a unique tracking key. This ensures that rate limits are isolated per user and per scope, preventing one project or organization from affecting another. - ### Endpoint exceptions Some endpoints have stricter rate limits than the standard 120 requests per minute to prevent abuse of resource-intensive operations: @@ -55,8 +64,6 @@ Some endpoints have stricter rate limits than the standard 120 requests per minu | `DELETE /v1/projects/:ref/config/custom-hostname` | 10 requests | 1 minute | These operations are expensive | | `GET /v1/projects/:ref/config/vanity-subdomain` | 10 requests | 1 minute | These operations are expensive | -**Note:** The `GET /v1/projects/:ref/database/context` endpoint has dual rate limiting. You can make up to 10 requests per minute, but also no more than 1 request per second to prevent burst traffic. - Some endpoints have the standard 120 requests per minute but with different timeout durations: | Endpoint | Limit | Duration | Reason | @@ -65,7 +72,7 @@ Some endpoints have the standard 120 requests per minute but with different time ### Best practices -- **Monitor rate limit headers** - Check the `X-RateLimit-Remaining` header to see how many requests you have left. When it approaches 0, slow down your requests to avoid hitting the limit. -- **Implement exponential backoff** - When you receive a 429 response, wait before retrying. You can use the `X-RateLimit-Reset` header (seconds) to determine exactly how long to wait. -- **Batch operations** - Where possible, combine multiple operations into fewer API calls to reduce your request count. -- **Be mindful of expensive endpoints** - Analytics, database context, and domain endpoints have stricter limits, so use them judiciously. +- **Monitor rate limit headers**: Check the `X-RateLimit-Remaining` header to see how many requests you have left. When it approaches 0, slow down your requests to avoid hitting the limit. +- **Implement exponential backoff**: When you receive a 429 response, wait before retrying. You can use the `X-RateLimit-Reset` header (seconds) to determine exactly how long to wait. +- **Batch operations**: Where possible, combine multiple operations into fewer API calls to reduce your request count. +- **Be mindful of expensive endpoints**: Analytics, database context, and domain endpoints have stricter limits, so use them sparingly. diff --git a/apps/docs/content/guides/ai-tools/mcp.mdx b/apps/docs/content/guides/ai-tools/mcp.mdx index bc47c66540a..361850963d7 100644 --- a/apps/docs/content/guides/ai-tools/mcp.mdx +++ b/apps/docs/content/guides/ai-tools/mcp.mdx @@ -4,6 +4,7 @@ title: 'Supabase MCP Server' subtitle: 'Connect your AI tools to Supabase using MCP' description: 'Connect your AI tools to Supabase using MCP' sidebar_label: 'MCP Server' +tocVideo: 1SMldLoOhbg --- The [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) is a standard for connecting Large Language Models (LLMs) to platforms like Supabase. Once connected, your AI assistants can interact with and query your Supabase projects on your behalf. diff --git a/apps/docs/content/guides/ai.mdx b/apps/docs/content/guides/ai.mdx index 263f0a17ae4..28a2c1874d0 100644 --- a/apps/docs/content/guides/ai.mdx +++ b/apps/docs/content/guides/ai.mdx @@ -4,6 +4,7 @@ title: 'AI & Vectors' description: 'The best vector database is the database you already have.' subtitle: 'The best vector database is the database you already have.' hideToc: true +tocVideo: 6xchFWcHkaI --- Supabase provides an open source toolkit for developing AI applications using Postgres and pgvector. Use the Supabase client libraries to store, index, and query your vector embeddings at scale. diff --git a/apps/docs/content/guides/ai/concepts.mdx b/apps/docs/content/guides/ai/concepts.mdx index dee95b3cd66..44eca61f4e9 100644 --- a/apps/docs/content/guides/ai/concepts.mdx +++ b/apps/docs/content/guides/ai/concepts.mdx @@ -3,6 +3,7 @@ id: 'ai-concepts' title: 'Concepts' description: 'Learn about embeddings within AI and vector applications.' sidebar_label: 'Concepts' +tocVideo: 6xchFWcHkaI --- Embeddings are core to many AI and vector applications. This guide covers these concepts. If you prefer to get started right away, see our guide on [Generating Embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings). diff --git a/apps/docs/content/guides/ai/semantic-search.mdx b/apps/docs/content/guides/ai/semantic-search.mdx index 2cd438f12d4..3fdb5e7436d 100644 --- a/apps/docs/content/guides/ai/semantic-search.mdx +++ b/apps/docs/content/guides/ai/semantic-search.mdx @@ -4,6 +4,7 @@ title: 'Semantic search' description: 'Learn how to search by meaning rather than exact keywords.' subtitle: 'Learn how to search by meaning rather than exact keywords.' sidebar_label: 'Semantic search' +tocVideo: 6xchFWcHkaI --- Semantic search interprets the meaning behind user queries rather than exact [keywords](/docs/guides/ai/keyword-search). It uses machine learning to capture the intent and context behind the query, handling language nuances like synonyms, phrasing variations, and word relationships. diff --git a/apps/docs/content/guides/api/rest/client-libs.mdx b/apps/docs/content/guides/api/rest/client-libs.mdx index b810676c8a1..a0b687c8ce9 100644 --- a/apps/docs/content/guides/api/rest/client-libs.mdx +++ b/apps/docs/content/guides/api/rest/client-libs.mdx @@ -15,12 +15,12 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie | Dart/Flutter | [supabase-flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter) | [Docs](/docs/reference/dart/introduction) | | Swift | [supabase-swift](https://github.com/supabase/supabase-swift) | [Docs](/docs/reference/swift/introduction) | | Python | [supabase-py](https://github.com/supabase/supabase-py) | [Docs](/docs/reference/python/initializing) | +| C# | [supabase-csharp](https://github.com/supabase/supabase-csharp) | [Docs](/docs/reference/csharp/introduction) | ## Community libraries | `Language` | `Source Code` | `Documentation` | | ----------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------- | -| C# | [supabase-csharp](https://github.com/supabase-community/supabase-csharp) | [Docs](/docs/reference/csharp/introduction) | | Go | [supabase-go](https://github.com/supabase-community/supabase-go) | | | Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](/docs/reference/kotlin/introduction) | | Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | | diff --git a/apps/docs/content/guides/auth.mdx b/apps/docs/content/guides/auth.mdx index fd4071f7e65..141452347ce 100644 --- a/apps/docs/content/guides/auth.mdx +++ b/apps/docs/content/guides/auth.mdx @@ -3,7 +3,7 @@ id: 'auth' title: 'Auth' description: 'Use Supabase to authenticate and authorize your users.' subtitle: 'Use Supabase to authenticate and authorize your users.' -tocVideo: '6ow_jW4epf8' +tocVideo: 'Ufx6fdRMxjU' --- Supabase Auth makes it easy to implement authentication and authorization in your app. We provide client SDKs and API endpoints to help you create and manage users. diff --git a/apps/docs/content/guides/auth/jwts.mdx b/apps/docs/content/guides/auth/jwts.mdx index b0ce408422e..e99569f9f65 100644 --- a/apps/docs/content/guides/auth/jwts.mdx +++ b/apps/docs/content/guides/auth/jwts.mdx @@ -2,6 +2,7 @@ id: 'auth-jwts' title: 'JSON Web Token (JWT)' subtitle: 'Information on how best to use JSON Web Tokens with Supabase' +tocVideo: 'Ufx6fdRMxjU' --- A [JSON Web Token](https://jwt.io/introduction) is a type of data structure, represented as a string, that usually contains identity and authorization information about a user. It encodes information about its lifetime and is signed with a cryptographic key to make it tamper-resistant. diff --git a/apps/docs/content/guides/auth/passwords.mdx b/apps/docs/content/guides/auth/passwords.mdx index f51864f058d..d5b0d33ec11 100644 --- a/apps/docs/content/guides/auth/passwords.mdx +++ b/apps/docs/content/guides/auth/passwords.mdx @@ -1,6 +1,7 @@ --- title: 'Password-based Auth' subtitle: 'Allow users to sign in with a password connected to their email or phone number.' +tocVideo: 'Ufx6fdRMxjU' --- Users often expect to sign in to your site with a password. Supabase Auth helps you implement password-based auth safely, using secure configuration options and best practices for storing and verifying passwords. diff --git a/apps/docs/content/guides/auth/social-login.mdx b/apps/docs/content/guides/auth/social-login.mdx index cdcb87370fe..d12678d731b 100644 --- a/apps/docs/content/guides/auth/social-login.mdx +++ b/apps/docs/content/guides/auth/social-login.mdx @@ -1,6 +1,7 @@ --- title: 'Social login' description: 'Signing in with social accounts' +tocVideo: 'Ufx6fdRMxjU' --- Social login (OAuth) is an open standard for authentication that allows users to sign in to one website or application using their credentials from another website or application. OAuth allows users to grant third-party applications access to their online accounts without sharing their passwords. diff --git a/apps/docs/content/guides/auth/third-party/custom-oidc.mdx b/apps/docs/content/guides/auth/third-party/custom-oidc.mdx new file mode 100644 index 00000000000..cc56295b4cb --- /dev/null +++ b/apps/docs/content/guides/auth/third-party/custom-oidc.mdx @@ -0,0 +1,109 @@ +--- +id: 'auth-third-party-custom-oidc' +title: 'Custom OIDC provider' +subtitle: 'Use any OIDC-compliant identity provider with your Supabase project' +--- + +Any identity provider that supports OpenID Connect (OIDC) can be used as a third-party authentication provider alongside Supabase Auth, or standalone, with your Supabase project. Use this guide for a provider that is not in the list of [supported providers](/docs/guides/auth/third-party/overview). + +## Getting started + +1. Check that your provider meets the [requirements](#requirements). +2. Add a Third-Party Auth integration with your provider's issuer URL in the [Dashboard](#add-the-integration-in-the-dashboard) or with the [Management API](#add-the-integration-with-the-management-api). +3. Add the `role` claim to the JWTs your provider issues. +4. Set up the Supabase client in your application. + +## Requirements + +Your provider must meet the [limitations](/docs/guides/auth/third-party/overview#limitations) of third-party auth: + +1. It exposes an OIDC Issuer Discovery URL, served at `/.well-known/openid-configuration`. +2. It signs JWTs with an asymmetric key and includes a `kid` header parameter. Symmetrically signed JWTs, such as HS256, are not supported. +3. Its JWTs contain a `role` claim. See [Assign the role claim](#assign-the-role-claim). + +The issuer URL must use HTTPS. Your project reads the signing keys from the `jwks_uri` listed in the discovery document. + +## Add the integration in the Dashboard + +1. In the Dashboard, go to **Authentication** > **Sign In / Providers** > [**Third-Party Auth**](/dashboard/project/_/auth/third-party). +2. Click **Add provider** and select **Custom OIDC**. +3. Enter your provider's issuer URL in **Issuer URL**, for example `https://login.example.com`. +4. Click **Add provider**. + +The provider appears in the Third-Party Auth list with its issuer URL. To remove it, delete it from the same list. + +## Add the integration with the Management API + +Create a [personal access token](/docs/guides/platform/personal-access-tokens), then call the [create third-party auth integration](/docs/reference/api/v1-create-project-tpa-integration) endpoint with your provider's issuer URL in the `oidc_issuer_url` field. To list or remove integrations, use the [list](/docs/reference/api/v1-list-project-tpa-integrations) and [delete](/docs/reference/api/v1-delete-project-tpa-integration) endpoints. + +## Supabase CLI + +Custom OIDC providers are not available in the Supabase CLI. There is no `supabase/config.toml` option for them. + +## Assign the role claim + +Your Supabase project inspects the `role` claim in every JWT sent to it, to assign the correct Postgres role when using the Data API, Storage or Realtime authorization. Without a `role` claim, requests run as the `anon` role. + +Set `role` to `authenticated` for signed-in users. Use your provider's custom claims or token template feature to add it. See your provider's documentation for the steps. + +## Set up the Supabase client library + +Pass an `accessToken` async function that returns your provider's JWT for the current user, or `null` if there is no signed-in user: + +```typescript +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient( + 'https://.supabase.co', + 'SUPABASE_PUBLISHABLE_KEY', + { + accessToken: async () => { + return getAccessTokenFromYourProvider() + }, + } +) +``` + +Replace `getAccessTokenFromYourProvider` with the call your provider's SDK offers to get the current user's JWT. + +## Using RLS policies + +After the client sends your provider's JWT, use RLS policies to secure access to your project's database, Storage objects and Realtime channels. + +### Identify the user + +The `sub` claim identifies the user. `auth.uid()` casts `sub` to `uuid`, so it fails if your provider issues subjects that are not UUIDs. Read the claim directly instead: + +```sql +create policy "Users can read their own rows" + on table_name + for select + to authenticated + using (user_id = (select auth.jwt()->>'sub')); +``` + +Here `user_id` must be a `text` column. + +### Restrict access by issuer + +Your project trusts every issuer you add, and `sub` values from different issuers can collide. If you use more than one issuer, or you also use Supabase Auth, add a restrictive policy that checks the `iss` claim: + +```sql +create policy "Restrict access to trusted issuers" + on table_name + as restrictive + to authenticated + using ( + (auth.jwt()->>'iss' = 'https://.supabase.co/auth/v1') + or + (auth.jwt()->>'iss' = 'https://login.example.com') + ); +``` + + + +Postgres has two types of policies: permissive and restrictive. This example uses a restrictive policy, so make sure you don't omit the `as restrictive` clause. + + + +For more on restrictive policies and on sharing the check across tables, Storage and Realtime, see the [Firebase Auth guide](/docs/guides/auth/third-party/firebase-auth#adding-an-extra-layer-of-security-to-your-projects-rls-policies-self-hosting-only). diff --git a/apps/docs/content/guides/auth/third-party/overview.mdx b/apps/docs/content/guides/auth/third-party/overview.mdx index f2fcb750241..84dc91ccd0f 100644 --- a/apps/docs/content/guides/auth/third-party/overview.mdx +++ b/apps/docs/content/guides/auth/third-party/overview.mdx @@ -11,6 +11,7 @@ Supabase has first-class support for these third-party authentication providers: - [Auth0](/docs/guides/auth/third-party/auth0) - [AWS Cognito (with or without AWS Amplify)](/docs/guides/auth/third-party/aws-cognito) - [WorkOS](/docs/guides/auth/third-party/workos) +- [Custom OIDC provider](/docs/guides/auth/third-party/custom-oidc), for any other provider that supports OpenID Connect You can use these providers alongside Supabase Auth, or on their own, to access the [Data API (REST and GraphQL)](/docs/guides/database/overview), [Storage](/docs/guides/storage), [Realtime](/docs/guides/storage) and [Functions](/docs/guides/functions) from your existing apps. diff --git a/apps/docs/content/guides/cli.mdx b/apps/docs/content/guides/cli.mdx deleted file mode 100644 index 65484bc4c96..00000000000 --- a/apps/docs/content/guides/cli.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -id: 'cli' -title: 'Local Dev with CLI' -description: 'Developing locally using the Supabase CLI.' -subtitle: 'Developing locally using the Supabase CLI.' -sidebar_label: 'Overview' ---- - -You can use the Supabase CLI to run the entire Supabase stack locally on your machine, by running `supabase init` and then `supabase start`. To install the CLI, read [the installation guide](/docs/guides/cli/getting-started#installing-the-supabase-cli). - -The Supabase CLI provides tools to develop your project locally, deploy to the Supabase Platform, handle database migrations, and generate types directly from your database schema. - -## Resources - -
-
- - - The Supabase CLI provides tools to develop manage your Supabase projects from your local - machine. - - -
-
- - - A GitHub action for interacting with your Supabase projects using the CLI. - - -
-
diff --git a/apps/docs/content/guides/cron.mdx b/apps/docs/content/guides/cron.mdx index ca52b0a431b..caa97f6a2f9 100644 --- a/apps/docs/content/guides/cron.mdx +++ b/apps/docs/content/guides/cron.mdx @@ -1,6 +1,7 @@ --- title: 'Cron' subtitle: 'Schedule Recurring Jobs with Cron Syntax in Postgres' +tocVideo: miRQPbIJOuQ --- Supabase Cron is a Postgres Module that simplifies scheduling recurring Jobs with cron syntax and monitoring Job runs inside Postgres. diff --git a/apps/docs/content/guides/cron/install.mdx b/apps/docs/content/guides/cron/install.mdx index 478bf9eec70..1089671e03c 100644 --- a/apps/docs/content/guides/cron/install.mdx +++ b/apps/docs/content/guides/cron/install.mdx @@ -1,5 +1,6 @@ --- title: 'Install' +tocVideo: miRQPbIJOuQ --- Install the Supabase Cron Postgres Module to begin scheduling recurring Jobs. diff --git a/apps/docs/content/guides/cron/quickstart.mdx b/apps/docs/content/guides/cron/quickstart.mdx index 616aedbb84a..eb55bad153c 100644 --- a/apps/docs/content/guides/cron/quickstart.mdx +++ b/apps/docs/content/guides/cron/quickstart.mdx @@ -1,5 +1,6 @@ --- title: 'Quickstart' +tocVideo: miRQPbIJOuQ --- diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index 68d781622ec..d05232ca547 100644 --- a/apps/docs/content/guides/database/extensions/postgis.mdx +++ b/apps/docs/content/guides/database/extensions/postgis.mdx @@ -29,7 +29,7 @@ You can get started with PostGIS by enabling the PostGIS extension in your Supab 1. Go to the [Database](/dashboard/project/_/database/tables) page in the Dashboard. 2. Click on **Extensions** in the sidebar. 3. Search for `postgis` and enable the extension. -4. In the confirmation prompt select "Create a new schema" and name it `gis` for example. +4. In the confirmation dialog, review the default `extensions` schema and click **Enable**. diff --git a/apps/docs/content/guides/database/overview.mdx b/apps/docs/content/guides/database/overview.mdx index 953c3bfe296..a0ce47c5572 100644 --- a/apps/docs/content/guides/database/overview.mdx +++ b/apps/docs/content/guides/database/overview.mdx @@ -4,6 +4,7 @@ title: 'Database' subtitle: 'Use Supabase to connect, manage, and secure your Postgres database.' description: 'Use Supabase to connect, manage, and secure your Postgres database.' sidebar_label: 'Overview' +tocVideo: 'C9kJwxhdw9A' --- Every Supabase project gets a full [Postgres](https://www.postgresql.org/) database, not a Postgres abstraction. This database is the foundation that Auth, Storage, Realtime, and Edge Functions are built on, and Supabase manages daily database backups and offers point-in-time recovery on paid plans. diff --git a/apps/docs/content/guides/database/postgres/triggers.mdx b/apps/docs/content/guides/database/postgres/triggers.mdx index 81cc59964aa..0b657a3a19b 100644 --- a/apps/docs/content/guides/database/postgres/triggers.mdx +++ b/apps/docs/content/guides/database/postgres/triggers.mdx @@ -3,6 +3,7 @@ id: 'postgres-triggers' title: 'Postgres Triggers' description: 'Automatically execute SQL on table events.' subtitle: 'Automatically execute SQL on table events.' +tocVideo: 'C9kJwxhdw9A' --- In Postgres, a trigger executes a set of actions automatically on table events such as INSERTs, UPDATEs, DELETEs, or TRUNCATE operations. diff --git a/apps/docs/content/guides/database/replication/pipelines/ducklake.mdx b/apps/docs/content/guides/database/replication/pipelines/ducklake.mdx index d6ff3179cd8..44aef146fe8 100644 --- a/apps/docs/content/guides/database/replication/pipelines/ducklake.mdx +++ b/apps/docs/content/guides/database/replication/pipelines/ducklake.mdx @@ -16,7 +16,12 @@ Insert-only tables don't require a primary key or replica identity. Updates and ## Prepare DuckLake resources [#understand-the-ducklake-components] -DuckLake stores metadata in a Postgres catalog and data in object storage. With **Select Supabase projects**, choose a project for the catalog, then a project and bucket for storage. The catalog and storage can use the same project. Pipelines creates the connection credentials. With **Enter connection details**, you provide the catalog URL and storage credentials. You also need a compatible query engine to read the replicated data. +DuckLake stores metadata in a Postgres catalog and data in object storage. Pipelines offers two ways to set them up: + +- **Use this Supabase project**: Pipelines uses this project's Postgres database as the catalog and a Storage bucket in the same project as object storage. It also creates the connection credentials. +- **Enter connection details**: You provide the catalog URL and the storage credentials. + +To read the replicated data, you also need a compatible query engine. | Component | Purpose | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- | @@ -30,27 +35,24 @@ You can query replicated tables, but treat them and their underlying catalog and Follow [Set up Pipelines](/docs/guides/database/replication/pipelines#setup-overview). In **Add pipeline**, select **DuckLake**, enter a **Pipeline name**, choose a **Publication**, and set **Initial sync**. Then choose how to configure the catalog and storage. -### Select Supabase projects [#use-supabase] +### Use this Supabase project [#use-supabase] -Choose Supabase projects for the Postgres catalog and Storage bucket. Pipelines creates the connection credentials when you create the destination. +Use the current Supabase project's Postgres database for the catalog and a Storage bucket in the same project. Pipelines creates the connection credentials when you create the destination. Before you begin: -- Choose active, healthy, non-branch projects from the same organization for the catalog and storage. You can use the same project for both. -- Make sure your organization role can administer SQL in the catalog project and Storage in the storage project. - Choose a Files bucket for DuckLake data. You can create a private bucket from the destination form. -- Choose an unused metadata schema name for this DuckLake. Pipelines creates the schema and its tables. Use only letters, numbers, and underscores. If the catalog project already uses the default `ducklake` schema for Warehouse, enter a different name. -- Keep catalog and storage in the same region when possible, near the [managed pipeline region](/docs/guides/database/replication/pipelines#region). +- Choose an unused metadata schema name for this DuckLake. Pipelines creates the schema and its tables. Use only letters, numbers, and underscores. If the project already uses the default `ducklake` schema for Warehouse, enter a different name. To configure the destination: -1. Select **Select Supabase projects** under **Configuration method**. -2. Choose the **Catalog project** and **Metadata schema**. -3. Choose the **Storage project** and **Bucket**. Select **New bucket** at the bottom of the bucket list if you haven't created one yet. +1. Select **Use this Supabase project** under **Configuration method**. +2. Enter the **Metadata schema**. +3. Choose the **Bucket**. Select **New bucket** at the bottom of the bucket list if you haven't created one yet. 4. Optionally adjust **Pool size** under **Advanced settings**. It allows 1 to 6 concurrent DuckDB connections; the default is 4. 5. Click **Start pipeline**. Review any validation warnings, then confirm the estimated cost with **Create and start pipeline**. -Credential-provisioning warnings are expected before creation: catalog and Storage credentials are provisioned when you save the destination. Review the selected resources before proceeding. +Credential-provisioning warnings are expected before creation: catalog and Storage credentials are provisioned when you save the destination. Review the metadata schema and bucket before proceeding. ### Enter connection details [#custom-parameters] @@ -62,7 +64,7 @@ Prepare the following resources: 2. An S3-compatible bucket and a dedicated prefix for this DuckLake. 3. Object-storage credentials that can list, read, write, and delete objects under that prefix. Delete access is required for managed file cleanup. -Choose an unused metadata schema name and a new data prefix for each destination. Pipelines creates the schema and its tables. Initialising a DuckLake catalog in that schema beforehand triggers a validation warning; reusing its schema or prefix can mix metadata or files from different DuckLakes. +Choose an unused metadata schema name and a new data prefix for each destination. Pipelines creates the schema and its tables. Initializing a DuckLake catalog in that schema beforehand triggers a validation warning; reusing its schema or prefix can mix metadata or files from different DuckLakes. Select **Enter connection details** under **Configuration method**, then configure these fields: @@ -91,7 +93,7 @@ A source `TRUNCATE` truncates the DuckLake table. A [table restart](/docs/guides Connect DuckDB with its `ducklake` extension, or another compatible engine, to the same catalog and storage path. Query through the catalog; reading raw Parquet files can miss inlined changes, delete files, and the current snapshot. -If you selected Supabase projects, create separate read credentials for the catalog and Storage projects. The writer credentials generated for Pipelines aren't exposed. If you entered connection details, use separate read-only credentials when your catalog and storage provider support them. +If you use this Supabase project, create separate read credentials for its Postgres database and Storage. The writer credentials generated for Pipelines aren't exposed. If you enter connection details, use separate read-only credentials when your catalog and storage provider support them. After attaching the catalog under an alias such as `my_ducklake`, source schemas and tables are available as qualified DuckLake tables: @@ -161,7 +163,7 @@ For type changes, unsupported changes, and interrupted schema changes, see the s | Issue | Resolution | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Credential-provisioning warnings | Expected when you [select Supabase projects](#use-supabase) before saving. Review the selected resources. | +| Credential-provisioning warnings | Expected when you [use this Supabase project](#use-supabase) before saving. Review the metadata schema and bucket. | | Catalog or storage validation fails | Check the [connection details](#custom-parameters), credentials, connectivity, and permissions for the configured prefix. Local `file://` paths are unsupported. | | Metadata schema exists | Choose a new schema, unless intentionally reusing the same DuckLake and its corresponding data path. | | Inserts work but updates or deletes fail | Check [replica identity and published columns](#source-table-requirements). | diff --git a/apps/docs/content/guides/database/tables.mdx b/apps/docs/content/guides/database/tables.mdx index aefc75980bc..98227034667 100644 --- a/apps/docs/content/guides/database/tables.mdx +++ b/apps/docs/content/guides/database/tables.mdx @@ -2,7 +2,7 @@ id: 'tables' title: 'Tables and data' description: 'Creating and using Postgres tables.' -video: 'https://www.youtube.com/v/TKwF3IGij5c' +tocVideo: 'C9kJwxhdw9A' --- Learn what tables are and how to use them. diff --git a/apps/docs/content/guides/database/views.mdx b/apps/docs/content/guides/database/views.mdx index 10656ecaecc..3074a2cc764 100644 --- a/apps/docs/content/guides/database/views.mdx +++ b/apps/docs/content/guides/database/views.mdx @@ -2,6 +2,7 @@ id: 'views' title: 'Views' description: 'Creating and using views in Postgres.' +tocVideo: 'C9kJwxhdw9A' --- Learn what views are and when to use them. diff --git a/apps/docs/content/guides/deployment/branching.mdx b/apps/docs/content/guides/deployment/branching.mdx index d4f3ed3ca11..db01c8e408d 100644 --- a/apps/docs/content/guides/deployment/branching.mdx +++ b/apps/docs/content/guides/deployment/branching.mdx @@ -7,27 +7,42 @@ You can deploy directly from GitHub on any plan — connect your repository and Use branching to safely experiment with changes to your Supabase project. -Supabase branches create separate environments that spin off from your main project. You can use these branching environments to create and test changes like new configurations, database schemas, or features without affecting your production setup. When you're ready to ship your changes, merge your branch to update your production instance with the new changes. +Supabase branches create separate environments that spin off from your base project. You can use these branching environments to create and test changes like new configurations, database schemas, or features without affecting your production setup. When you're ready to ship your changes, merge your branch to update your production instance with the new changes. ## How branching works - **Separate Environments**: Each branch is a separate environment with its own Supabase instance and API credentials. - **Preview Branches**: Preview branches are ephemeral and best suited for focused testing. They are automatically deleted when a PR is merged or closed. - **Persistent Branches**: Persistent branches are long-lived and recommended for environments like staging, QA, or development. They aren't automatically paused or deleted due to inactivity or when a PR is merged or closed. -- **Managing Branches**: You can create, review, and merge branches either automatically via our [GitHub integration](/docs/guides/deployment/branching/github-integration) or directly [through the dashboard](/docs/guides/deployment/branching/dashboard) (currently in beta). All branches show up in the branches page in the dashboard, regardless of how they were created. -- **Cloned from your main project**: Each new branch is created as a clone of your main project. It starts with that project's Edge Functions deployed and the configuration set. -- **Data-less by default**: By default, new branches do not start with any data or storage objects from your main project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration, or the [Include data](/docs/guides/deployment/branching/dashboard#include-production-data) option if you create the branch from the dashboard. +- **Managing Branches**: You can create, review, and merge branches either automatically via our [GitHub integration](/docs/guides/deployment/branching/github-integration) or directly [through the Supabase Dashboard](/docs/guides/deployment/branching/dashboard) (currently in beta). All branches show up in the branches page in the dashboard, regardless of how they were created. +- **Cloned from your base project**: Each new branch is created as a clone of your base project. It starts with that project's Edge Functions deployed and the configuration set. +- **Data-less by default**: By default, new branches do not start with any data or storage objects from your base project. This is meant to better protect your sensitive production data. To start your branches with data, you can use a [seed file](/docs/guides/deployment/branching/github-integration#seeding) if using the GitHub integration, or the [Include data](/docs/guides/deployment/branching/dashboard#include-production-data) option if you create the branch from the Supabase Dashboard. + +## What a new branch starts with + +Whether you create a branch from a pull request or from the Supabase Dashboard, Supabase creates a project with default settings then copies these settings from your base project: + +- Postgres settings +- Connection pooler settings +- Auth, Data API, Realtime, and Storage settings +- SSL enforcement +- Network restrictions, when your base project has them +- Deployed Edge Functions + +Auth secrets, such as OAuth provider secrets and SMTP passwords, get placeholder values instead. Set the real values with [branch secrets](/docs/guides/deployment/branching/configuration#managing-secrets-for-branches). Supabase also turns on Database Webhooks for every branch. + +The database schema isn't cloned. A GitHub-connected branch builds it from the [migrations in your repository](/docs/guides/deployment/branching/github-integration#migrations). A dashboard branch builds it from your [base project's migration history](/docs/guides/deployment/branching/dashboard#where-a-dashboard-branch-gets-its-schema). The branch is ready when the settings, the schema, and the Edge Functions are all in place. ## Deploying to production -When you merge any branch into your main project, Supabase automatically runs a deployment workflow to deploy your changes to production. The deployment workflow is expressed as a Directed Acyclic Graph where each node represents one of the following deployment steps. +When you merge any branch into your base project, Supabase automatically runs a deployment workflow to deploy your changes to production. The deployment workflow is expressed as a Directed Acyclic Graph where each node represents one of the following deployment steps. 1. **Clone** - Checks out your repository at the specified git branch (optional for [Branching via Dashboard](/docs/guides/deployment/branching/dashboard)) -2. **Pull** - Retrieves database migrations from your main project (also initialises the migration history table when Branching via Dashboard) +2. **Pull** - Retrieves database migrations from your base project (also initialises the migration history table when Branching via Dashboard) 3. **Health** - Waits up to 2 minutes for all Supabase services on your branch to be running and healthy, including Auth, API, Database, Storage, and Realtime 4. **Configure** - Updates service configurations based on your config.toml file (only available for [Branching via GitHub](/docs/guides/deployment/branching/github-integration)) 5. **Migrate** - Applies pending database migrations and vault secrets to your branch 6. **Seed** - Runs seed files to populate your branch with initial data (must be [enabled in config.toml](/docs/guides/deployment/branching/configuration#branch-configuration-with-remotes) for persistent branches) 7. **Deploy** - Deploys any changed Edge Functions and updates function secrets -If a parent deployment step fails, all dependent child steps will be skipped. For instance, if your database migrations failed at step 5, our runner will not seed your branch because step 6 is skipped. If you are using GitHub integration, the same deployment workflow will be run on every commit pushed to your git branch. +If a parent deployment step fails, all dependent child steps are skipped. For instance, if your database migrations fail at step 5, our runner doesn't seed your branch because step 6 is skipped. If you are using GitHub integration, the same deployment workflow runs on every commit pushed to your git branch. To check each step's status in the Supabase Dashboard, see [Monitoring deployments](/docs/guides/deployment/branching/troubleshooting#monitoring-deployments). diff --git a/apps/docs/content/guides/deployment/branching/dashboard.mdx b/apps/docs/content/guides/deployment/branching/dashboard.mdx index e10efb1c2ce..767c6540f6d 100644 --- a/apps/docs/content/guides/deployment/branching/dashboard.mdx +++ b/apps/docs/content/guides/deployment/branching/dashboard.mdx @@ -3,17 +3,17 @@ title: 'Branching via the dashboard' subtitle: 'Create, manage, review, and merge branches directly in the dashboard' --- -You can create, manage, review, and merge Supabase branches directly via the dashboard. This is useful for quick testing, prototyping, or when you prefer to work in a no-code way. You can also connect a Supabase branch to a GitHub branch at a later time if needed. +You can create, manage, review, and merge Supabase branches directly in the Supabase Dashboard. This is useful for quick testing, prototyping, or when you prefer to work in a no-code way. You can also connect a Supabase branch to a GitHub branch at a later time if needed. -Branch management via the dashboard is currently in public alpha. Features and functionality may change. +Branch management in the Supabase Dashboard is currently in public alpha. Features and functionality may change. ## How Branching works -You can do the following directly from the Supabase dashboard: +You can do the following directly from the Supabase Dashboard: - Create preview branches - Make changes to your public schema or edge functions @@ -41,7 +41,11 @@ Once you've enabled the feature, you can create a new branch: 1. Click the arrows next to the branch name in the top menu bar. (The top menu bar has the format `YOUR_ORGANIZATION / YOUR_PROJECT / CURRENT_BRANCH_NAME`.) 2. Click `Create branch`. -The new branch is a clone of your base project. It starts with the project's schema, Edge Functions, and the configuration set. +The new branch is a clone of your base project. It starts with the project's schema, Edge Functions, and the configuration set. For the full list of what it inherits, see [What a new branch starts with](/docs/guides/deployment/branching#what-a-new-branch-starts-with). + +### Where a dashboard branch gets its schema + +A dashboard branch has no git branch to read migrations from. Instead, it reads your base project's migration history and applies each entry as a migration. The branch ends up with the same schema as your base project. If your base project has no migration history, the branch dumps the base project's schema and uses the dump as its first migration. ### Include production data @@ -81,7 +85,7 @@ When reviewing a merge request you may see a notice at the top of the page askin There are a few limitations you should be aware of before deciding to use branching without git. -- Custom roles created through the dashboard are not captured on branch creation +- Custom roles created through the Supabase Dashboard are not captured on branch creation - Branches can only be merged to main; merging between preview branches is not supported - If your branch is out of date, you can pull in latest changes from main, but keep in mind that all functions will be overwritten - Deleting functions must be done manually on main branch diff --git a/apps/docs/content/guides/deployment/branching/github-integration.mdx b/apps/docs/content/guides/deployment/branching/github-integration.mdx index c9ae8315c2d..74e2e265a7a 100644 --- a/apps/docs/content/guides/deployment/branching/github-integration.mdx +++ b/apps/docs/content/guides/deployment/branching/github-integration.mdx @@ -43,7 +43,7 @@ You will be using the [Supabase CLI](/docs/guides/local-development) to initiali - Pull your database changes using `supabase db pull`. To get your database connection string, go to your project dashboard, click [Connect](/dashboard/project/_?showConnect=true&method=session) and look for the Session pooler connection string. + Pull your database changes using `supabase db pull`. To get your database connection string, open the Supabase Dashboard, click [Connect](/dashboard/project/_?showConnect=true&method=session) and look for the Session pooler connection string. ```markdown supabase db pull --db-url @@ -83,13 +83,23 @@ Enable the **Automatic branching** option in your GitHub Integration configurati When a new branch is created in GitHub, a corresponding branch is created in Supabase. (You can enable the **Supabase changes only** option to only create Supabase branches when Supabase files change.) -Every Supabase branch, preview or persistent, is created as a clone of your base project. The new branch starts with the Edge Functions and configuration of that project. Its database schema is not cloned. Instead, it is built from the migrations you commit to your repository. +Every Supabase branch, preview or persistent, is created as a clone of your base project. The new branch starts with the Edge Functions and configuration of that project. Its database schema is not cloned. Instead, it is built from the migrations you commit to your repository. For the full list of what a branch inherits, see [What a new branch starts with](/docs/guides/deployment/branching#what-a-new-branch-starts-with). + +### What runs on each push + +After Supabase creates the branch and copies your base project's settings, it runs the branching workflow against the files in your repository. The workflow applies your `config.toml` first, then runs any migrations the branch hasn't applied yet. After that it seeds the database and Storage buckets, and finally deploys your Edge Functions. On a persistent branch, including your production branch, the `config.toml` and seed steps apply only when a `[remotes]` block names that branch, as described in [Configuration](#configuration). The same workflow runs again on every push to the git branch. You don't need a GitHub Action to deploy migrations or sync `config.toml` to a branch. + +For what each step does, see [Deploying to production](/docs/guides/deployment/branching#deploying-to-production). To check a step's status and read its logs, see [Monitoring deployments](/docs/guides/deployment/branching/troubleshooting#monitoring-deployments). ### Configuration You can test configuration changes on your Preview Branch by configuring the `config.toml` file in your Supabase directory. See the [Configuration docs](/docs/guides/deployment/branching/configuration) for more information. -Your branch starts with the configuration of your base project. The settings in your `config.toml` file are applied on top of that clone. +Supabase applies the settings in your `config.toml` on top of the configuration copied from your base project, and where the two differ, your repository wins. + +The `configure` step compares the `[db]`, `[api]`, `[auth]`, `[storage]`, and `[experimental]` sections of `config.toml` with the branch's current settings and updates the ones that differ. The migrate, seed, and deploy steps read the other sections, such as `[functions]` and `[db.seed]`. If your repository has no `config.toml`, the step only turns on Database Webhooks. + +On a persistent branch, including your production branch, the step runs only when a `[remotes]` block names that branch's project ID. For the `[remotes]` syntax, see [Remote-specific configuration](/docs/guides/deployment/branching/configuration#remote-specific-configuration). A comment is added to your PR with the deployment status of your preview branch. @@ -97,11 +107,18 @@ A comment is added to your PR with the deployment status of your preview branch. The migrations in the `migrations` subdirectory of your Supabase directory are automatically run when the branch is created. Each later commit runs only the migrations that haven't been applied yet. +The `migrate` step compares the files in `supabase/migrations` with the branch's migration history and applies every file without a history entry. Before it runs them, it saves the Vault secrets declared under `[db.vault]` to the branch. Keep two cases in mind: + +- A migration file with a timestamp older than the latest applied migration still runs and the step finishes with a warning. +- A version in the branch's migration history with no matching file fails the step. + +To stop branching from running migrations, set `enabled = false` under `[db.migrations]` in `config.toml`. + If your project uses [declarative schemas](/docs/guides/local-development/declarative-database-schemas), the files in `supabase/schemas/` are not applied by branching. Run `supabase db schema declarative sync` to generate a migration from your schema changes, and commit the generated migration alongside your schema files. Branching runs each migration file inside a single transaction and does not honor the `-- pg-delta: transaction=false` directive. A migration that can't run in a transaction (for example, one that uses `create index concurrently`, or a [`pg-delta`](/docs/guides/local-development/diff-engines) file that carries the directive) applies with `supabase db push` but fails when branching deploys it. -If you want to rerun existing migrations, reset the branch from the Supabase dashboard to start from scratch. Note that existing data on your branch will also be dropped by a reset. +If you want to rerun existing migrations, reset the branch from the Supabase Dashboard to start from scratch. Note that existing data on your branch will also be dropped by a reset. ### Seeding @@ -111,6 +128,15 @@ You can seed your Preview Branch with sample data using the `seed.sql` file in y Data changes in your seed files are not merged to production. +### Edge Functions + +Edge Functions reach your branch in the `deploy` step, the last step of the workflow. The step saves the function secrets declared under `[edge_runtime.secrets]` then deploys functions: + +- If your repository has a `supabase/functions` directory, the step deploys the functions declared in `config.toml` under `[functions.]`. If the directory has functions but `config.toml` declares none, the step deploys nothing and finishes with a warning. +- If your repository has no functions directory, the step copies the functions deployed on your base project. `config.toml` can still disable a copied function or change its JWT verification. + +A function is uploaded again only when it changed. For each function, the step compares the checksum of the new bundle and the JWT verification setting against the version already deployed on the branch. If both are the same, the step skips that function. + ## Deploying changes to production Enable the **Deploy to production** option in your GitHub Integration configuration to automatically deploy changes when you push or merge to production branch. diff --git a/apps/docs/content/guides/deployment/branching/troubleshooting.mdx b/apps/docs/content/guides/deployment/branching/troubleshooting.mdx index 8970d6daada..06f1b79c4dc 100644 --- a/apps/docs/content/guides/deployment/branching/troubleshooting.mdx +++ b/apps/docs/content/guides/deployment/branching/troubleshooting.mdx @@ -10,11 +10,23 @@ This guide covers common issues you might encounter when using Supabase branchin To check deployment status and troubleshoot failures: -1. Go to your project dashboard +1. Open the Supabase Dashboard 2. Navigate to "Manage Branches" 3. Click on your branch to view deployment logs 4. Check the "View logs" section for detailed error messages +Each step in a workflow run reports one of these statuses: + +| Status | Meaning | +| --------- | ----------------------------------------------------------------------------------- | +| `CREATED` | The step hasn't started. | +| `RUNNING` | The step is in progress. | +| `EXITED` | The step succeeded. | +| `PAUSED` | The step finished with a warning, or was skipped on purpose. Later steps still run. | +| `DEAD` | The step failed. Steps that depend on it don't run. | + +A failed health check is a warning. The `health` step shows `PAUSED` and the workflow continues. A `DEAD` `migrate` step stops the `seed` and `deploy` steps. + For programmatic monitoring, you can use the [Management API](https://api.supabase.com/api/v1#tag/environments/post/v1/projects/{ref}/branches) to poll branch status. For detailed troubleshooting guidance, see our [Troubleshooting guide](/docs/guides/deployment/branching/troubleshooting). @@ -29,7 +41,7 @@ To fix this, push the latest changes, then delete the preview branch in Supabase The new preview branch is a fresh clone of your base project and is reseeded from the `./supabase/seed.sql` file by default. Any additional data changes made on the old preview branch are lost. -To rerun migrations that your base project has already applied, reset the branch from the Supabase dashboard instead. A reset reruns all migrations in sequential order and drops existing data on the branch. +To rerun migrations that your base project has already applied, reset the branch from the Supabase Dashboard instead. A reset reruns all migrations in sequential order and drops existing data on the branch. ### Deployment failures @@ -123,6 +135,8 @@ If configuration changes aren't applying: 1. **Syntax errors**: Validate your `config.toml` syntax 2. **Git sync**: Ensure changes are committed and pushed 3. **Branch refresh**: Try deleting and recreating the branch +4. **Persistent branch**: Configuration runs on a persistent branch, including your production branch, only when a `[remotes]` block names its project ID. For the `[remotes]` syntax, see [Remote-specific configuration](/docs/guides/deployment/branching/configuration#remote-specific-configuration) +5. **Section not applied by branching**: The `configure` step applies only the `[db]`, `[api]`, `[auth]`, `[storage]`, and `[experimental]` sections. For which steps read the other sections, see [Configuration](/docs/guides/deployment/branching/github-integration#configuration) ### Secrets not available @@ -172,7 +186,7 @@ Remember that preview branch data: If you're still experiencing issues: -1. **Check logs**: Review branch logs in the dashboard +1. **Check logs**: Review branch logs in the Supabase Dashboard 2. **Community**: Ask in [GitHub discussions](https://github.com/orgs/supabase/discussions/18937) 3. **Support**: Contact support for project-specific issues 4. **Documentation**: Review the latest documentation for updates diff --git a/apps/docs/content/guides/functions.mdx b/apps/docs/content/guides/functions.mdx index 2982243fa10..192c4ac632c 100644 --- a/apps/docs/content/guides/functions.mdx +++ b/apps/docs/content/guides/functions.mdx @@ -4,7 +4,7 @@ title: 'Edge Functions' description: 'Run TypeScript functions globally at the edge.' subtitle: 'Run TypeScript functions globally at the edge.' sidebar_label: 'Overview' -tocVideo: 'za_loEtS4gs' +tocVideo: 'EQ5h3NSWMbk' --- Edge Functions are server-side TypeScript functions, distributed globally at the edge—close to your users. They can be used for listening to webhooks or integrating your Supabase project with third-parties [like Stripe](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/stripe-webhooks). Edge Functions are developed using [Deno](https://deno.com), which offers a few benefits to you as a developer: diff --git a/apps/docs/content/guides/functions/architecture.mdx b/apps/docs/content/guides/functions/architecture.mdx index 219233e37f6..cfa48ba10da 100644 --- a/apps/docs/content/guides/functions/architecture.mdx +++ b/apps/docs/content/guides/functions/architecture.mdx @@ -3,7 +3,7 @@ id: 'architecture' title: 'Edge Functions Architecture' description: 'Guide to Supabase Edge Functions: Architecture and How They Work' subtitle: 'Understanding the Architecture of Supabase Edge Functions' -tocVideo: 'za_loEtS4gs' +tocVideo: 'EQ5h3NSWMbk' --- This guide explains the architecture and inner workings of Supabase Edge Functions, based on the concepts demonstrated in the video "Supabase Edge Functions Explained". Edge functions are serverless compute resources that run at the edge of the network, close to users, enabling low-latency execution for tasks like API endpoints, webhooks, and real-time data processing. This guide breaks down Edge Functions into key sections: an example use case, deployment process, global distribution, and execution mechanics. diff --git a/apps/docs/content/guides/functions/dependencies.mdx b/apps/docs/content/guides/functions/dependencies.mdx index 2bb5a5f62d6..b6ffc5aa266 100644 --- a/apps/docs/content/guides/functions/dependencies.mdx +++ b/apps/docs/content/guides/functions/dependencies.mdx @@ -115,6 +115,8 @@ To use private npm packages, create a `.npmrc` file within your function’s own This feature requires Supabase CLI version 1.207.9 or higher. +Your function's directory must also contain a `deno.json` or `package.json` file. The file can be empty. If neither file is present, the `.npmrc` file is ignored. This matches the Deno CLI behavior. + ```bash └── supabase └── functions diff --git a/apps/docs/content/guides/functions/deploy.mdx b/apps/docs/content/guides/functions/deploy.mdx index eeb9812a161..593523210c5 100644 --- a/apps/docs/content/guides/functions/deploy.mdx +++ b/apps/docs/content/guides/functions/deploy.mdx @@ -3,7 +3,7 @@ id: 'functions-deploy' title: 'Deploy to Production' description: 'Deploy your Edge Functions to your remote Supabase Project.' subtitle: 'Deploy your Edge Functions to your remote Supabase Project.' -tocVideo: '5OWH9c4u68M' +tocVideo: 'EQ5h3NSWMbk' --- Once you have developed your Edge Functions locally, you can deploy them to your Supabase project. diff --git a/apps/docs/content/guides/functions/development-environment.mdx b/apps/docs/content/guides/functions/development-environment.mdx index 903ba2c2ffe..35d64e12e70 100644 --- a/apps/docs/content/guides/functions/development-environment.mdx +++ b/apps/docs/content/guides/functions/development-environment.mdx @@ -3,7 +3,7 @@ id: 'development-environment' title: 'Development Environment' description: 'Get the best Edge Functions experience with the right local developer environment.' subtitle: 'Set up your local development environment for Edge Functions.' -tocVideo: 'lFhU3L8VoSQ' +tocVideo: 'EQ5h3NSWMbk' --- diff --git a/apps/docs/content/guides/functions/quickstart-dashboard.mdx b/apps/docs/content/guides/functions/quickstart-dashboard.mdx index 60c641f0ddf..a297ba2e862 100644 --- a/apps/docs/content/guides/functions/quickstart-dashboard.mdx +++ b/apps/docs/content/guides/functions/quickstart-dashboard.mdx @@ -3,6 +3,7 @@ id: 'functions-quickstart-dashboard' title: 'Getting Started with Edge Functions (Dashboard)' description: 'Get started with Supabase Edge Functions.' subtitle: 'Learn how to create, test, and deploy your first Edge Function using the Supabase Dashboard.' +tocVideo: 'EQ5h3NSWMbk' --- Supabase allows you to create Supabase Edge Functions directly from the Supabase Dashboard, making it easy to deploy functions without needing to set up a local development environment. The Edge Functions editor in the Dashboard has built-in syntax highlighting and type-checking for Deno and Supabase-specific APIs. diff --git a/apps/docs/content/guides/functions/quickstart.mdx b/apps/docs/content/guides/functions/quickstart.mdx index 91eb4ff3e23..2dd784f9aab 100644 --- a/apps/docs/content/guides/functions/quickstart.mdx +++ b/apps/docs/content/guides/functions/quickstart.mdx @@ -3,6 +3,7 @@ id: 'functions-quickstart' title: 'Getting Started with Edge Functions' description: 'Get started with Supabase Edge Functions.' subtitle: 'Learn how to create, test, and deploy your first Edge Function using the Supabase CLI.' +tocVideo: 'EQ5h3NSWMbk' --- This guide walks you through creating, testing locally, deploying, and invoking a Supabase Edge Function using the CLI. By the end, you'll have a working function running on Supabase's global edge network. diff --git a/apps/docs/content/guides/functions/secrets.mdx b/apps/docs/content/guides/functions/secrets.mdx index cd5e1ed8d28..1116c06861c 100644 --- a/apps/docs/content/guides/functions/secrets.mdx +++ b/apps/docs/content/guides/functions/secrets.mdx @@ -3,6 +3,7 @@ id: 'functions-secrets' title: 'Environment variables' description: 'Set, read, and deploy Edge Function secrets.' subtitle: 'Store API keys and other secrets where your Edge Functions can read them.' +tocVideo: 'EQ5h3NSWMbk' --- Local development and production load secrets differently, so set them in both. diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index ede5583753d..fd13a7f9e27 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -3,6 +3,7 @@ id: 'getting-started' title: 'Getting Started' description: 'Resources for getting started with Supabase.' hideToc: true +tocVideo: i_bPeTZVlg0 --- diff --git a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx index d94dfad6429..74a3e80a775 100644 --- a/apps/docs/content/guides/integrations/supabase-for-platforms.mdx +++ b/apps/docs/content/guides/integrations/supabase-for-platforms.mdx @@ -170,12 +170,6 @@ curl 'https://api.supabase.com/v1/projects/{ref}/branches' \ Management API endpoint: [`POST /v1/projects/{ref}/database/migrations`](https://api.supabase.com/api/v1#tag/database/post/v1/projects/{ref}/database/migrations) - - -Only select customers have access to database migrations endpoint. Submit this [form](/solutions/ai-builders#talk-to-partnerships-team) to get access. - - - For this example we will create a todos table using the [`POST /v1/projects/{ref}/database/migrations`](https://api.supabase.com/api/v1#tag/database/post/v1/projects/{ref}/database/migrations) endpoint. ```sql diff --git a/apps/docs/content/guides/integrations/vercel-marketplace.mdx b/apps/docs/content/guides/integrations/vercel-marketplace.mdx index 26c4be43088..f2c1ffcb47c 100644 --- a/apps/docs/content/guides/integrations/vercel-marketplace.mdx +++ b/apps/docs/content/guides/integrations/vercel-marketplace.mdx @@ -32,7 +32,7 @@ Vercel Marketplace is currently in Public Alpha. If you encounter any issues or ### Via Vercel Marketplace -Details coming soon.. +Go to the [Supabase page](https://vercel.com/marketplace/supabase) in the Vercel Marketplace. ### Connecting to Supabase project diff --git a/apps/docs/content/guides/local-development.mdx b/apps/docs/content/guides/local-development.mdx index 652707b56bc..ded657c6113 100644 --- a/apps/docs/content/guides/local-development.mdx +++ b/apps/docs/content/guides/local-development.mdx @@ -1,6 +1,7 @@ --- title: Local Development & CLI subtitle: Learn how to develop locally and use the Supabase CLI +tocVideo: _2N6ApZ0MmI --- To develop your applications using the locally running Supabase stack, you'll need to install the [Supabase CLI](#cli) and a container runtime. diff --git a/apps/docs/content/guides/local-development/cli-workflows.mdx b/apps/docs/content/guides/local-development/cli-workflows.mdx index e37770639c8..7f1908601a1 100644 --- a/apps/docs/content/guides/local-development/cli-workflows.mdx +++ b/apps/docs/content/guides/local-development/cli-workflows.mdx @@ -3,6 +3,7 @@ id: 'cli-workflows' title: 'Local development workflow' description: 'Set up and run your day-to-day local development workflow with the Supabase CLI.' subtitle: 'Set up and run your day-to-day local development workflow with the Supabase CLI.' +tocVideo: _2N6ApZ0MmI --- This guide walks through two common starting points for local development with the Supabase CLI, and shows how they converge into the same daily workflow. By the end, you'll have a `./supabase` directory in your repo that anyone can clone to recreate the full project, locally or on a fresh remote instance. 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 70b24e7c33d..a673a69ff0d 100644 --- a/apps/docs/content/guides/local-development/cli/getting-started.mdx +++ b/apps/docs/content/guides/local-development/cli/getting-started.mdx @@ -2,6 +2,7 @@ title: 'Supabase CLI' description: 'The Supabase CLI provides tools to develop your project locally, deploy to the Supabase Platform, and set up CI/CD workflows.' subtitle: 'Develop locally, deploy to the Supabase Platform, and set up CI/CD workflows' +tocVideo: _2N6ApZ0MmI --- The Supabase CLI runs the entire Supabase stack on your own machine or in a CI environment, so you can build and test locally, then connect your project to a hosted one when you're ready to deploy. @@ -109,7 +110,7 @@ Linux packages are provided in [Releases](https://github.com/supabase/cli/releas ### Run a local Supabase project [#running-a-local-supabase-project] -The most common thing you'll do with the CLI is run the full Supabase stack (Postgres, Auth, Storage, and the rest) on your own machine. That stack runs in Docker containers, so you need a container runtime installed first. On Windows and Linux, follow the official guide to install and configure [Docker Desktop](https://docs.docker.com/desktop). +The most common thing you do with the CLI is run the full Supabase stack (Postgres, Auth, Storage, and the rest) on your own machine. That stack runs in Docker containers, so you need a container runtime installed first. On Windows and Linux, follow the official guide to install and configure [Docker Desktop](https://docs.docker.com/desktop). In a coding agent sandbox or on a CI runner with no container engine, the experimental native runtime runs each service as a process instead. It supports Linux and macOS on Apple silicon. For platform requirements and setup, see [Docker and native runtimes](/docs/guides/local-development/docker-and-native-runtimes). On macOS, we recommend [OrbStack](https://orbstack.dev/) instead of Docker Desktop. It's a drop-in replacement that handles extended file attributes (xattrs) on mounted volumes and container networking more reliably than Docker Desktop. It also starts faster and uses less CPU, memory, and disk, which makes a noticeable difference when running the full Supabase stack. diff --git a/apps/docs/content/guides/local-development/database-migrations.mdx b/apps/docs/content/guides/local-development/database-migrations.mdx index 954df881534..140a16ca695 100644 --- a/apps/docs/content/guides/local-development/database-migrations.mdx +++ b/apps/docs/content/guides/local-development/database-migrations.mdx @@ -3,8 +3,7 @@ id: 'database-migrations' title: 'Database migrations' description: 'Track and version your database schema changes with migrations.' subtitle: 'Track and version your database schema changes with migrations.' -video: 'https://www.youtube-nocookie.com/v/vyHyYpvjaks' -tocVideo: 'vyHyYpvjaks' +tocVideo: '_2N6ApZ0MmI' --- Supabase is a flexible platform that lets you decide how you want to build your projects. You can use the Dashboard directly to get up and running, or use a proper local setup. We suggest you work locally and deploy your changes to a linked project on the [Supabase Platform](https://app.supabase.io/). diff --git a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx index 1aa9181e746..e82961d30b7 100644 --- a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx +++ b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx @@ -91,7 +91,7 @@ supabase migration up With declarative schemas, the files in `supabase/schemas/` are the source of truth. `supabase db schema declarative sync` compares those files against your migrations. It does not read the live database. Changes you make directly (Studio, the SQL editor, `psql`) are invisible to the diff, which reports "No schema changes found" and silently drops the change. Always edit the schema files, then run `sync`. -Don't use `supabase db diff` here, because it diffs a live database against your migrations and never uses `supabase/schemas/` as its baseline. +Don't use `supabase db diff` here, because it diffs a live database against your migrations and never uses `supabase/schemas/` as its baseline. If you run `supabase db diff -f` while schema files exist, the CLI prints a note that it ignored them. When your migrations are empty or outdated, the migration it writes can include objects your schema files already declare. @@ -283,6 +283,15 @@ supabase db schema declarative generate --linked This writes per-object SQL files under `supabase/schemas/` (or the directory set by `experimental.pgdelta.declarative_schema_path`), organized by database schema. In scripts, pass the target explicitly (`--local`, `--linked`, or `--db-url`) and add `--overwrite` to replace an existing tree without prompting. To later refresh the declarative tree from the remote database (for example, after a change was deployed outside your local workflow), run `supabase db pull --declarative`. It replaces the schema files without creating a migration or touching migration history. + + +CLI versions before 2.115.0 wrote the declarative tree to `supabase/database`. Version 2.115.0 and later read `supabase/schemas` and warn when `supabase/database` holds schema files, `supabase/schemas` is empty, and `declarative_schema_path` is unset. To clear the warning, do one of the following: + +- Move the tree to `supabase/schemas/`. +- Keep the tree in place and set `declarative_schema_path = "./database"` under `[experimental.pgdelta]`. + + + `generate` writes schema files only. Because `sync` diffs your schema files against your migration history, that history must describe the same database before your first `sync`. If your project has no migrations yet, run `supabase db pull` first to create a baseline migration. Without a baseline, the first `sync` regenerates the entire schema as one migration. That migration can apply cleanly to an empty local database and still fail on `db push`, because the remote database already has those objects. ### Rolling back a schema change @@ -309,7 +318,7 @@ Schema diffs are generated by `pg-delta`, which models most database entities, i ### Data manipulation language -DML statements such as `insert`, `update`, and `delete` are never captured by a schema diff. This includes storage buckets, which are rows in the `storage.buckets` table rather than schema objects. A DML statement inside a declarative schema file is an error. Keep data changes in [seed files](/docs/guides/local-development/seeding-your-database) or hand-written [versioned migrations](/docs/guides/deployment/database-migrations). +DML statements such as `insert`, `update`, and `delete` are never captured by a schema diff. This includes storage buckets, which are rows in the `storage.buckets` table rather than schema objects. A DML statement inside a declarative schema file is an error. Keep data changes in [seed files](/docs/guides/local-development/seeding-your-database) or hand-written [versioned migrations](/docs/guides/deployment/database-migrations). Supported [extension configuration](#extension-managed-objects), such as named `pg_cron` jobs, is tracked separately through extension functions. ### Default privileges on new objects @@ -331,6 +340,10 @@ Manage those objects through versioned migrations. Objects that belong to an extension are recognized as the extension's, not yours. Anything an extension owns is excluded from diffs. Objects extensions create as they run, such as partitions maintained by `pg_partman` and queue tables created by `pgmq`, are never emitted as raw `create table` or `drop table` statements. The diff expresses changes to them through the extension's own API instead, such as `select pgmq.drop_queue('q');` or a `delete from partman.part_config` row, and the CLI flags those statements as destructive. Create and change these objects through the extension's own functions, and review any generated API calls before committing. +`pg-delta` tracks named `pg_cron` jobs as extension configuration, even though they are stored as rows in `cron.job`. Generated migrations create jobs with `select cron.schedule_in_database(...)` and remove them with `select cron.unschedule(...)`. For a changed job, the migration calls `cron.unschedule(...)` followed by `cron.schedule_in_database(...)`. + +With `pg_cron` enabled, declare each job in a SQL file under `supabase/schemas/` using `select cron.schedule('', ...)`. Give each job a unique name. The engine skips jobs with a missing or duplicate `jobname` and reports a warning. To change or remove a job, edit or remove its declaration, then [generate and apply a migration](#updating-your-schema) with `supabase db schema declarative sync`. + ### Adopting an existing schema tree Two checks gate `sync` on a schema tree the CLI didn't generate: @@ -344,7 +357,12 @@ Projects that haven't enabled `pg-delta` can still use declarative schemas. The -On the legacy engine, don't run `supabase db pull --declarative`. The command runs regardless of the `[experimental.pgdelta]` setting, but the tree it writes is only readable by the `db schema declarative` commands. `db diff` on the legacy engine tries to load that tree as its baseline, in lexicographic order, and fails on the first file. If you've already run it, delete the generated tree under `supabase/schemas/` or [switch to `pg-delta`](#switching-to-pg-delta). +On the legacy engine, don't run `supabase db pull --declarative`. The command runs regardless of the `[experimental.pgdelta]` setting and writes a per-object tree meant for the `db schema declarative` commands. It also points `[db.migrations].schema_paths` in `config.toml` at that tree. The next local `db diff` applies the files in lexicographic order without resolving dependencies between them, so it fails when a file sorts before an object it depends on. + +If you've already run it, [switch to `pg-delta`](#switching-to-pg-delta), or undo both changes: + +1. Delete the files the export wrote to the declarative directory (`supabase/schemas/` by default). Files that were already there are left in place. +2. Restore `[db.migrations].schema_paths` in `config.toml` to its previous value, or remove it if the export added it. diff --git a/apps/docs/content/guides/local-development/diff-engines.mdx b/apps/docs/content/guides/local-development/diff-engines.mdx index e357a2ea0da..f8180dafafe 100644 --- a/apps/docs/content/guides/local-development/diff-engines.mdx +++ b/apps/docs/content/guides/local-development/diff-engines.mdx @@ -24,12 +24,15 @@ enabled = true Existing projects without that section keep the legacy `migra` engine until they add it. See [Switch an existing project to `pg-delta`](#switch-an-existing-project-to-pg-delta). -To use `migra` for a single run without changing `config.toml`, the flag depends on the command: +To switch engines for a single run without changing `config.toml`, the flag depends on the command: -| Command | Flag to fall back to `migra` | -| --------- | ---------------------------- | -| `db diff` | `--use-migra` | -| `db pull` | `--diff-engine migra` | +| Command | Flag to use `pg-delta` | Flag to use `migra` | +| ------------------------------------------- | ------------------------ | --------------------- | +| `db diff` | `--use-pg-delta` | `--use-migra` | +| `db pull` | `--diff-engine pg-delta` | `--diff-engine migra` | +| `db schema declarative generate` and `sync` | `--experimental` | Not available | + +For `db diff` and `db pull`, setting `SUPABASE_EXPERIMENTAL_PG_DELTA=1` in your shell or in `supabase/.env` has the same effect as `enabled = true`. An explicit engine flag on the command still wins. To opt out entirely, set `enabled = false` under `[experimental.pgdelta]`. @@ -45,7 +48,7 @@ The commands and the migration workflow are the same on both engines. What diffe | Ordering of declarative files | Automatic, based on dependencies between statements. `[db.migrations].schema_paths` is ignored, and the CLI warns when the setting lists any paths. | Lexicographic by default. `[db.migrations].schema_paths` overrides the order. Files in `supabase/schemas/` are used even when the setting is empty. | | Initial `db pull` (empty migration history) | Diffs the remote database against an empty shadow database. Your customizations in managed schemas, such as triggers on `auth.users` and RLS policies on `storage.objects`, are included. | Seeds the migration with `pg_dump`, which skips managed schemas, then appends a diff of your triggers and RLS policies in `auth` and `storage`. Older versions excluded those schemas and told you to run `supabase db pull --schema auth,storage`. | | Later `db pull` runs | Diffs the remote database against your migrations. | Same. Trigger and RLS policy changes in managed schemas are included. | -| `db pull --declarative` | Replaces `supabase/schemas/` from the selected database without creating a migration. | Runs, but don't use it. The tree it writes is only readable by the `db schema declarative` commands. `db diff` on the legacy engine then tries to load that tree as its baseline and fails. | +| `db pull --declarative` | Replaces `supabase/schemas/` from the selected database without creating a migration. | Runs, but don't use it. The command also points `[db.migrations].schema_paths` at the tree it writes. The next local `db diff` applies those files in lexicographic order and fails when a file sorts before an object it depends on. | | Objects the engine doesn't track | Reported as warnings and never silently dropped. Pass `--strict-coverage` to turn the warnings into failures. | Silently omitted. `--strict-coverage` has no effect. | | Generated SQL | Uppercase keywords wrapped at 180 characters. Configurable with `[experimental.pgdelta] format_options`. | Engine default formatting. | | Migration files per run | Usually one. When a change crosses a transaction boundary, it may write one ordered file per unit with `_1`, `_2` suffixes. Files whose statements can't run in a transaction start with `-- pg-delta: transaction=false`. | One file. | diff --git a/apps/docs/content/guides/local-development/docker-and-native-runtimes.mdx b/apps/docs/content/guides/local-development/docker-and-native-runtimes.mdx index 113bc12b773..0a8356bcd5f 100644 --- a/apps/docs/content/guides/local-development/docker-and-native-runtimes.mdx +++ b/apps/docs/content/guides/local-development/docker-and-native-runtimes.mdx @@ -110,5 +110,6 @@ The `runtime` field is `docker`, `podman`, or `native`. The text output of `supa - A local project keeps the runtime it was created with, and the CLI doesn't move data between runtimes. See [How the CLI picks a runtime](#how-the-cli-picks-a-runtime). - Running several native local projects on one machine works, but they share host resources without the isolation containers provide. Use Docker for that case. - If Postgres takes too long to become ready on a cold start, raise `health_timeout` under `[db]` in `config.toml`. Both runtimes use that setting. +- In CLI v2.121.0 or later, the native runtime runs each `pg_cron` job in a background worker instead of over a new connection. As a result, a job body can't contain its own `BEGIN` or `COMMIT`. Earlier versions fail every native `pg_cron` job with `connection failed` or with `job startup timeout` on macOS. diff --git a/apps/docs/content/guides/local-development/running-multiple-local-projects.mdx b/apps/docs/content/guides/local-development/running-multiple-local-projects.mdx index e718ac1c1b8..87036186f78 100644 --- a/apps/docs/content/guides/local-development/running-multiple-local-projects.mdx +++ b/apps/docs/content/guides/local-development/running-multiple-local-projects.mdx @@ -106,7 +106,15 @@ For an app that already has a `config.toml`, remove the port lines from each app - `smtp_port` and `pop3_port` under `[local_smtp]`, and `vector_port` under `[analytics]`, if you set them. `supabase init` writes the SMTP ports commented out. 3. Save the file and commit it, so everyone on the team gets the same behavior. -Remove the ports before the app's first `supabase start` with the setting on. A local project keeps the ports it was created with. If you already started one with the fixed ports, removing them afterward makes `supabase start` fail with `cannot change on the saved stack`. Start a new local project with a different `--stack` name, or run `supabase stack destroy` and start again. Destroying deletes the local project's data. +If you already started a local project with the fixed ports: + +1. Stop it with `supabase stop`. +2. Remove the port keys from `config.toml`. +3. Run `supabase start` again. + +In CLI v2.121.0 or later, `supabase start` applies the port change to the stopped local project and prints each moved endpoint, such as `api: 54321 → 26199`. The local project keeps its data. + +Earlier versions fail with `The saved stack cannot adopt these changes`. On those versions, start a new local project with a different `--stack` name or run `supabase stack destroy` and start again. Destroying deletes the local project's data. The CLI also treats ports you set with an environment variable, such as `SUPABASE_API_PORT` or `SUPABASE_DB_PORT`, as exact requests. If another process already listens on that port, the start fails. When the port belongs to another local project, the error names that local project. @@ -146,7 +154,7 @@ The database can't be excluded. Studio needs the REST API, so to exclude `rest`, Running `supabase start` again on a running local project keeps its current settings and prints `Stack is already running with its current services`. To apply a change to `config.toml`, `--exclude`, or `--eager`, stop the local project and start it again. Starting without `--exclude` restores the services enabled in `config.toml`. -Stopping and starting doesn't apply changes to ports or to the Postgres major version. For those, start a new local project with a different `--stack` name, or run `supabase stack destroy` and start again. +In CLI v2.121.0 or later, stopping and starting also applies port changes. A change to the Postgres major version always needs a new local project. Start one with a different `--stack` name, or run `supabase stack destroy` and start again. ## Run named local projects for one app @@ -206,9 +214,15 @@ To list every local project on the machine, across all apps, run: supabase stack list ``` -The list shows each local project's name, project root, branch, runtime, and a short ID. Pass `--output-format json` to get the full ID that `--stack-id` accepts. +The list shows each local project's name, project root, branch, runtime, whether its background process is reachable, and a short ID. Pass `--output-format json` to get the full ID that `--stack-id` accepts. -To stream live logs from a local project, run `supabase stack logs`. Pass `--service database` to limit the output to one service. +To read a local project's logs, run `supabase stack logs` in its app directory. The command prints the last 200 retained lines and exits. It also works while the local project is stopped. These flags change what it prints: + +- `--follow` keeps streaming new lines after the history. +- `--tail ` changes how many retained lines it prints. +- `--since 10m` prints only lines from the last 10 minutes. +- `--service database` narrows the output to one service. +- `--service gateway` shows only the requests that reached the shared API port. ## Choose a runtime @@ -246,9 +260,11 @@ To permanently delete one local project and its data, run `supabase stack destro supabase stack destroy --stack test --yes ``` +In CLI v2.121.0 or later, you can destroy several local projects in one command by passing `--stack-id` more than once with IDs from `supabase stack list`. The command asks for confirmation once. Every ID must match a local project before it destroys anything. Earlier versions destroy one local project per command. + -`supabase stack destroy` deletes the local project's database data and frees its ports. There is no backup and no undo. There is also no bulk destroy, so one command removes one local project. +`supabase stack destroy` takes no backup, so you can't recover the local project's data afterward. It also frees the local project's ports. @@ -276,10 +292,10 @@ Keep these differences in mind when you turn the setting on for an app that used - Custom Auth email templates with `content_path` - Storage Analytics and Storage vector buckets - A custom `edge_runtime.deno_version` - - OrioleDB, and `db.major_version` values other than 15 and 17 + - `db.major_version` values other than 15 and 17 + - `db.orioledb_version` in CLI versions earlier than v2.121.0, and the `experimental.s3_*` settings for OrioleDB in any version - Database commands don't accept `--stack`. `supabase db reset` and `supabase db diff` run against the current branch's default local project. `supabase db pull`, `supabase db push`, and `supabase db dump` target the linked project unless you pass `--local`. To reach a named local project, pass the `DB_URL` from `supabase status --stack --env` to a command that accepts `--db-url`. - Switching branches switches local projects. The git branch is part of the identity. After you check out another branch in the same directory, `supabase start` creates or resumes a different local project with its own data. The first branch's local project keeps running until you stop it. - Moving or renaming an app directory creates a new local project, because the identity includes the project root path. The previous local project's data stays until you destroy it. -- `supabase stack logs` streams new log lines only. It has no history. diff --git a/apps/docs/content/guides/observability/log-drains.mdx b/apps/docs/content/guides/observability/log-drains.mdx index 968578c30aa..55b6a3fafaf 100644 --- a/apps/docs/content/guides/observability/log-drains.mdx +++ b/apps/docs/content/guides/observability/log-drains.mdx @@ -2,6 +2,7 @@ id: 'log-drains' title: 'Log Drains' description: 'Getting started with Supabase Log Drains' +tocVideo: A4GFmvgxS-E --- Log drains send all logs of the Supabase stack to one or more desired destinations. It is only available for customers on Pro, Team and Enterprise Plans. Log drains are available in the dashboard under [Project Settings > Log Drains](/dashboard/project/_/settings/log-drains). diff --git a/apps/docs/content/guides/platform/custom-domains.mdx b/apps/docs/content/guides/platform/custom-domains.mdx index 3ec9ff1b06d..799a79b4117 100644 --- a/apps/docs/content/guides/platform/custom-domains.mdx +++ b/apps/docs/content/guides/platform/custom-domains.mdx @@ -136,6 +136,8 @@ const supabase = createClient('https://api.example.com', 'sb_publishable_...') Similarly, your Edge Functions will now be available at `https://api.example.com/functions/v1/your_function_name`, and your Storage objects at `https://api.example.com/storage/v1/object/public/your_file_path.ext`. +Your custom domain supports the same TLS versions and cipher suites as your project's default domain. For details, see [TLS for project APIs](/docs/guides/security/tls). + ### Remove a custom domain Removing a custom domain may cause some issues when using Supabase Auth with OAuth or SAML. You may have to reverse the changes made in the _[Prepare to activate your domain](#prepare-to-activate-your-domain)_ step above. diff --git a/apps/docs/content/guides/platform/ssl-enforcement.mdx b/apps/docs/content/guides/platform/ssl-enforcement.mdx index f911eef9f05..773bb123174 100644 --- a/apps/docs/content/guides/platform/ssl-enforcement.mdx +++ b/apps/docs/content/guides/platform/ssl-enforcement.mdx @@ -5,7 +5,7 @@ description: 'Enforce SSL usage for all Postgres connections' Your Supabase project supports connecting to the Postgres DB without SSL enabled to maximize client compatibility. For increased security, you can prevent clients from connecting if they're not using SSL. -Disabling SSL enforcement only applies to connections to Postgres, Supavisor (shared Connection Pooler) and PgBouncer (dedicated Connection Pooler); all HTTP APIs offered by Supabase (e.g., PostgREST, Storage, Auth) automatically enforce SSL on all incoming connections. +Disabling SSL enforcement only applies to connections to Postgres, Supavisor (shared Connection Pooler) and PgBouncer (dedicated Connection Pooler); all HTTP APIs offered by Supabase (e.g., PostgREST, Storage, Auth) automatically enforce SSL on all incoming connections (see [TLS for project APIs](/docs/guides/security/tls)). diff --git a/apps/docs/content/guides/queues.mdx b/apps/docs/content/guides/queues.mdx index 2d69042c249..cec78bc6f46 100644 --- a/apps/docs/content/guides/queues.mdx +++ b/apps/docs/content/guides/queues.mdx @@ -1,6 +1,7 @@ --- title: Supabase Queues subtitle: 'Durable Message Queues with Guaranteed Delivery in Postgres' +tocVideo: UEwfaElBnZk --- Supabase Queues is a Postgres-native durable Message Queue system with guaranteed delivery built on the [pgmq database extension](https://github.com/tembo-io/pgmq). It offers developers a seamless way to persist and process Messages in the background while improving the resiliency and scalability of their applications and services. diff --git a/apps/docs/content/guides/queues/quickstart.mdx b/apps/docs/content/guides/queues/quickstart.mdx index f93effb9add..dc1f0bd5e65 100644 --- a/apps/docs/content/guides/queues/quickstart.mdx +++ b/apps/docs/content/guides/queues/quickstart.mdx @@ -1,6 +1,7 @@ --- title: Quickstart subtitle: 'Learn how to use Supabase Queues to add and read messages' +tocVideo: UEwfaElBnZk --- This guide is an introduction to interacting with Supabase Queues via the Dashboard and official client library. Check out [Queues API Reference](/docs/guides/queues/api) for more details on our API. diff --git a/apps/docs/content/guides/realtime.mdx b/apps/docs/content/guides/realtime.mdx index 8352d90659b..7bee8d87317 100644 --- a/apps/docs/content/guides/realtime.mdx +++ b/apps/docs/content/guides/realtime.mdx @@ -4,6 +4,7 @@ title: 'Realtime' description: 'Send and receive messages to connected clients.' subtitle: 'Send and receive messages to connected clients.' hideToc: true +tocVideo: _-y_Lnujq64 --- Supabase provides a globally distributed [Realtime](https://github.com/supabase/realtime) service with the following features: diff --git a/apps/docs/content/guides/realtime/broadcast.mdx b/apps/docs/content/guides/realtime/broadcast.mdx index 41240dafea0..3a3d029f7f0 100644 --- a/apps/docs/content/guides/realtime/broadcast.mdx +++ b/apps/docs/content/guides/realtime/broadcast.mdx @@ -2,6 +2,7 @@ title: 'Broadcast' subtitle: 'Send low-latency messages using the client libs, REST, or your Database.' description: 'Send low-latency messages using the client libs, REST, or your Database.' +tocVideo: _-y_Lnujq64 --- You can use Realtime Broadcast to send low-latency messages between users. Messages can be sent using the client libraries, REST APIs, or directly from your database. diff --git a/apps/docs/content/guides/realtime/getting_started.mdx b/apps/docs/content/guides/realtime/getting_started.mdx index 477f21740fb..09b7b3acc7b 100644 --- a/apps/docs/content/guides/realtime/getting_started.mdx +++ b/apps/docs/content/guides/realtime/getting_started.mdx @@ -4,6 +4,7 @@ title: 'Getting Started with Realtime' description: 'Learn how to build real-time applications with Supabase Realtime' subtitle: 'Learn how to build real-time applications with Supabase Realtime' sidebar_label: 'Getting Started' +tocVideo: _-y_Lnujq64 --- ## Quick start diff --git a/apps/docs/content/guides/realtime/postgres-changes.mdx b/apps/docs/content/guides/realtime/postgres-changes.mdx index e43943740d6..7f8a1748193 100644 --- a/apps/docs/content/guides/realtime/postgres-changes.mdx +++ b/apps/docs/content/guides/realtime/postgres-changes.mdx @@ -2,6 +2,7 @@ title: 'Postgres Changes' subtitle: 'Listen to Postgres changes using Supabase Realtime.' description: 'Listen to Postgres changes using Supabase Realtime.' +tocVideo: _-y_Lnujq64 --- Use Realtime's Postgres Changes to listen to database events. diff --git a/apps/docs/content/guides/realtime/presence.mdx b/apps/docs/content/guides/realtime/presence.mdx index db10f3f9089..e07e8893343 100644 --- a/apps/docs/content/guides/realtime/presence.mdx +++ b/apps/docs/content/guides/realtime/presence.mdx @@ -2,6 +2,7 @@ title: 'Presence' description: 'Share state between users with Realtime Presence.' subtitle: 'Share state between users with Realtime Presence.' +tocVideo: _-y_Lnujq64 --- Use Realtime Presence to track state between multiple users. diff --git a/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx b/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx index a30fdce0212..a474a37d92a 100644 --- a/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx +++ b/apps/docs/content/guides/realtime/subscribing-to-database-changes.mdx @@ -4,6 +4,7 @@ title: 'Subscribing to Database Changes' subtitle: 'Listen to database changes in real-time from your website or application.' description: 'Listen to database changes in real-time from your website or application.' sidebar_label: 'Videos' +tocVideo: _-y_Lnujq64 --- You can use Supabase to subscribe to real-time database changes. There are two options available: diff --git a/apps/docs/content/guides/security/platform-security.mdx b/apps/docs/content/guides/security/platform-security.mdx index f25f5e71220..473c7581dcb 100644 --- a/apps/docs/content/guides/security/platform-security.mdx +++ b/apps/docs/content/guides/security/platform-security.mdx @@ -41,6 +41,12 @@ For configuration information, see [Postgres SSL Enforcement](/docs/guides/platf +### TLS for project APIs + +All HTTP APIs of your project, including Auth, the Data API, Storage, Realtime, and Edge Functions, accept only HTTPS connections using TLS 1.2 or TLS 1.3. + +For supported cipher suites, see [TLS for project APIs](/docs/guides/security/tls). + ### Network restrictions Each Supabase project comes with configurable restrictions on the IP ranges that are allowed to connect to Postgres and its pooler ("your database"). These restrictions are enforced before traffic reaches the database. If a connection is not restricted by IP, it still needs to authenticate successfully with valid database credentials. diff --git a/apps/docs/content/guides/security/tls.mdx b/apps/docs/content/guides/security/tls.mdx new file mode 100644 index 00000000000..871145f2270 --- /dev/null +++ b/apps/docs/content/guides/security/tls.mdx @@ -0,0 +1,86 @@ +--- +id: 'tls' +title: 'TLS for project APIs' +description: 'TLS versions and cipher suites supported by Supabase project APIs, and the removal of CBC-mode ciphers.' +--- + +Your project's APIs accept only HTTPS connections. Use the TLS versions and cipher suites on this page to check whether a client can connect, or to answer findings from a security scan. + +These settings apply to your project's default domain, `.supabase.co`, and to [custom domains](/docs/guides/platform/custom-domains). It covers every service on that domain: Auth, the Data API, Storage, Realtime, and Edge Functions. For direct Postgres connections and connections through the pooler, see [Postgres SSL Enforcement](/docs/guides/platform/ssl-enforcement). + + + +On November 10, 2026, at the earliest, clients on old, unsupported operating systems, or embedded devices that haven't been updated might no longer be able to connect to your project's APIs. To check your clients, see [Removal of CBC-mode ciphers](#removal-of-cbc-mode-ciphers). + + + +## Supported TLS versions + +Project APIs accept connections using TLS 1.2 and TLS 1.3. SSL 3.0, TLS 1.0, and TLS 1.1 aren't supported. + +## Supported cipher suites + +Project APIs support all standard TLS 1.3 cipher suites. + +On TLS 1.2, project APIs offer the following cipher suites: + +| IANA name | OpenSSL name | +| ----------------------------------------------- | ------------------------------- | +| `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` | `ECDHE-ECDSA-AES128-GCM-SHA256` | +| `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` | `ECDHE-ECDSA-CHACHA20-POLY1305` | +| `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` | `ECDHE-RSA-AES128-GCM-SHA256` | +| `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` | `ECDHE-RSA-CHACHA20-POLY1305` | +| `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` | `ECDHE-ECDSA-AES256-GCM-SHA384` | +| `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` | `ECDHE-RSA-AES256-GCM-SHA384` | + +Until the removal date, project APIs also offer the deprecated CBC-mode cipher suites listed under [Removal of CBC-mode ciphers](#removal-of-cbc-mode-ciphers). + +## Removal of CBC-mode ciphers + +A cipher suite determines how a connection is encrypted. Cipher block chaining (CBC) mode cipher suites have known security weaknesses and security scanners commonly flag them. Supabase will stop offering the following TLS 1.2 cipher suites on project APIs **on November 10, 2026, at the earliest**: + +| IANA name | OpenSSL name | +| ----------------------------------------- | --------------------------- | +| `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256` | `ECDHE-ECDSA-AES128-SHA256` | +| `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256` | `ECDHE-RSA-AES128-SHA256` | +| `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384` | `ECDHE-ECDSA-AES256-SHA384` | +| `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384` | `ECDHE-RSA-AES256-SHA384` | + +The date might move later based on customer feedback. + +### Affected clients + +A client is affected if it can't use TLS 1.3 and the only TLS 1.2 cipher suites it shares with project APIs are the CBC-mode ones. For the cipher suites that remain, see [Supported cipher suites](#supported-cipher-suites). After the removal, it fails during the TLS handshake and can't connect to your project's APIs. + +In September 2026, fewer than 0.0001% of connections to Supabase projects used these cipher suites. Clients that depend on them typically run end-of-life software, for example: + +- Unsupported operating system versions +- Embedded devices, such as smart TVs or set-top boxes, that no longer receive updates +- HTTP clients or runtimes built against outdated TLS libraries + +### Unaffected connections + +The removal doesn't affect the following connections: + +- TLS 1.3 connections +- TLS 1.2 connections from clients that support at least one of the non-CBC cipher suites listed under [Supported cipher suites](#supported-cipher-suites) +- Direct Postgres connections and connections through the pooler +- The [Management API](/docs/reference/api/introduction) at `api.supabase.com` + +### Prepare for the removal + +1. List every client that connects to your project's APIs, on `.supabase.co` and on any custom domain. Include Realtime connections, servers, embedded devices, and third-party integrations. +2. Include every client version that's still in use, not only the latest release. Users might keep running older versions of your mobile or desktop apps, on operating systems that no longer receive updates. +3. For each client, check its operating system, runtime, and TLS library. See [Check whether a client is affected](#check-whether-a-client-is-affected). +4. Update affected clients. See [Update affected clients](#update-affected-clients). +5. After you update, test the application itself. Until the removal date, project APIs still offer CBC-mode cipher suites, so a request that succeeds now doesn't show that the client will keep working after the removal. + +### Check whether a client is affected + +Check the TLS configuration your application actually uses: its operating system, runtime, and TLS library versions, and any TLS version or cipher suite settings in your code or configuration. A client that connects with TLS 1.3 isn't affected, whatever TLS 1.2 cipher suites it supports. + +### Update affected clients + +Update the affected client's operating system, runtime, or TLS library to a version that supports TLS 1.3 or a supported TLS 1.2 cipher suite. For error messages and workarounds for clients you can't update, see [TLS handshake failures after removal of CBC-mode ciphers](/docs/guides/troubleshooting/tls-handshake-failures-after-cbc-cipher-removal). + +If you can't update your clients before the removal date, [open a support ticket](/dashboard/support/new). diff --git a/apps/docs/content/guides/self-hosting/self-hosted-envoy.mdx b/apps/docs/content/guides/self-hosting/self-hosted-envoy.mdx index 810946f9ed3..1f220e319f1 100644 --- a/apps/docs/content/guides/self-hosting/self-hosted-envoy.mdx +++ b/apps/docs/content/guides/self-hosting/self-hosted-envoy.mdx @@ -104,29 +104,31 @@ sh run.sh restart api-gw Routes are matched in the order declared. The first matching prefix wins. Protected routes require a valid `apikey` header; open routes pass through without API key validation. -| Path prefix | Upstream | Path rewrite | Access control | Notes | -| ----------------------------------------- | --------- | ------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------- | -| `/auth/v1/verify` | auth | `/verify` | Open | Email verification | -| `/auth/v1/callback` | auth | `/callback` | Open | OAuth callback | -| `/auth/v1/authorize` | auth | `/authorize` | Open | OAuth authorize | -| `/auth/v1/.well-known/jwks.json` | auth | `/.well-known/jwks.json` | Open | JWKS for third-party verification | -| `/.well-known/oauth-authorization-server` | auth | - | Open | OAuth 2.0 Authorization Server Metadata (RFC 8414) | -| `/auth/v1/sso/saml/acs` | auth | `/sso/saml/acs` | Open | SAML assertion consumer | -| `/auth/v1/sso/saml/metadata` | auth | `/sso/saml/metadata` | Open | SAML metadata | -| `/functions/v1/` | functions | `/` | `sb_` keys | Rejects invalid and conflicting `sb_` keys; others pass; 160s idle timeout, 410s upstream response timeout | -| `/storage/v1/` | storage | `/` | Bypass | Storage performs its own authorization | -| `/auth/v1/` | auth | `/` | API key | Protected Auth endpoints | -| `/rest/v1/` | rest | `/` | API key | PostgREST Open API root (requires secret key) | -| `/rest/v1/*` | rest | `/` | API key | PostgREST data endpoints | -| `/graphql/v1` | rest | `/rpc/graphql` | API key | pg_graphql (adds `Content-Profile: graphql_public`) | -| `/realtime/v1/api/tenants` | realtime | - | Denied | Realtime management API (blocked by default) | -| `/realtime/v1/api/openapi` | realtime | - | Denied | Realtime OpenAPI spec (blocked by default) | -| `/realtime/v1/api` | realtime | `/api` | API key | Realtime REST API (broadcast, channels, ping) | -| `/realtime/v1/` | realtime | `/socket/` | API key | Realtime WebSocket | -| `/pg/` | meta | `/` | API key | postgres-meta / Studio database operations (requires secret key) | -| `/api/mcp` | studio | - | Denied | MCP endpoint (blocked by default via RBAC DENY) | -| `/mcp` | studio | `/api/mcp` | Denied | MCP endpoint (blocked by default via RBAC DENY) | -| `/` (catch-all) | studio | - | Basic auth | Dashboard; strips inbound `Authorization` header | +| Path prefix | Upstream | Path rewrite | Access control | Notes | +| ------------------------------------------------- | --------- | ----------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- | +| `/auth/v1/verify` | auth | `/verify` | Open | Email verification | +| `/auth/v1/callback` | auth | `/callback` | Open | OAuth callback | +| `/auth/v1/authorize` | auth | `/authorize` | Open | OAuth authorize | +| `/auth/v1/.well-known/jwks.json` | auth | `/.well-known/jwks.json` | Open | JWKS for third-party verification | +| `/auth/v1/.well-known/oauth-authorization-server` | auth | `/.well-known/oauth-authorization-server` | Open | OAuth 2.0 Authorization Server Metadata | +| `/auth/v1/.well-known/openid-configuration` | auth | `/.well-known/openid-configuration` | Open | OpenID Connect discovery | +| `/.well-known/oauth-authorization-server/auth/v1` | auth | `/.well-known/oauth-authorization-server` | Open | OAuth 2.0 Authorization Server Metadata at the RFC 8414 path for the `/auth/v1` issuer | +| `/auth/v1/sso/saml/acs` | auth | `/sso/saml/acs` | Open | SAML assertion consumer | +| `/auth/v1/sso/saml/metadata` | auth | `/sso/saml/metadata` | Open | SAML metadata | +| `/functions/v1/` | functions | `/` | `sb_` keys | Rejects invalid and conflicting `sb_` keys; others pass; 160s idle timeout, 410s upstream response timeout | +| `/storage/v1/` | storage | `/` | Bypass | Storage performs its own authorization | +| `/auth/v1/` | auth | `/` | API key | Protected Auth endpoints | +| `/rest/v1/` | rest | `/` | API key | PostgREST Open API root (requires secret key) | +| `/rest/v1/*` | rest | `/` | API key | PostgREST data endpoints | +| `/graphql/v1` | rest | `/rpc/graphql` | API key | pg_graphql (adds `Content-Profile: graphql_public`) | +| `/realtime/v1/api/tenants` | realtime | - | Denied | Realtime management API (blocked by default) | +| `/realtime/v1/api/openapi` | realtime | - | Denied | Realtime OpenAPI spec (blocked by default) | +| `/realtime/v1/api` | realtime | `/api` | API key | Realtime REST API (broadcast, channels, ping) | +| `/realtime/v1/` | realtime | `/socket/` | API key | Realtime WebSocket | +| `/pg/` | meta | `/` | API key | postgres-meta / Studio database operations (requires secret key) | +| `/api/mcp` | studio | - | Denied | MCP endpoint (blocked by default via RBAC DENY) | +| `/mcp` | studio | `/api/mcp` | Denied | MCP endpoint (blocked by default via RBAC DENY) | +| `/` (catch-all) | studio | - | Basic auth | Dashboard; strips inbound `Authorization` header | diff --git a/apps/docs/content/guides/storage.mdx b/apps/docs/content/guides/storage.mdx index a918edf83a1..f68c79f88c6 100644 --- a/apps/docs/content/guides/storage.mdx +++ b/apps/docs/content/guides/storage.mdx @@ -5,6 +5,7 @@ description: 'Use Supabase to store and serve files.' subtitle: 'Use Supabase to store and serve files.' sidebar_label: 'Overview' hideToc: true +tocVideo: ch2UGHMreB0 --- Supabase Storage is a robust, scalable solution for managing files of any size with fine-grained access controls and optimized delivery. Whether you're storing user-generated content, analytics data, or vector embeddings, Supabase Storage provides specialized bucket types to meet your specific needs. diff --git a/apps/docs/content/guides/storage/buckets/creating-buckets.mdx b/apps/docs/content/guides/storage/buckets/creating-buckets.mdx index 2763b2ac01d..04a176d800d 100644 --- a/apps/docs/content/guides/storage/buckets/creating-buckets.mdx +++ b/apps/docs/content/guides/storage/buckets/creating-buckets.mdx @@ -3,6 +3,7 @@ id: 'storage-creating-buckets' title: 'Creating Buckets' description: 'Learn how to create Supabase Storage buckets.' sidebar_label: 'Buckets' +tocVideo: ch2UGHMreB0 --- You can create a bucket using the Supabase Dashboard. Since storage is interoperable with your Postgres database, you can also use SQL or our client libraries. diff --git a/apps/docs/content/guides/storage/buckets/fundamentals.mdx b/apps/docs/content/guides/storage/buckets/fundamentals.mdx index 190e678d6c4..aa420898fa7 100644 --- a/apps/docs/content/guides/storage/buckets/fundamentals.mdx +++ b/apps/docs/content/guides/storage/buckets/fundamentals.mdx @@ -3,6 +3,7 @@ id: 'storage-bucket-public-and-private' title: 'Storage Buckets' description: 'Learn how Supabase Storage Buckets works.' sidebar_label: 'Buckets' +tocVideo: ch2UGHMreB0 --- Buckets allow you to keep your files organized and determines the [Access Model](#access-model) for your assets. [Upload restrictions](/docs/guides/storage/buckets/creating-buckets#restricting-uploads) like max file size and allowed content types are also defined at the bucket level. diff --git a/apps/docs/content/guides/storage/quickstart.mdx b/apps/docs/content/guides/storage/quickstart.mdx index 70e8c5c94e5..ee15e95ed33 100644 --- a/apps/docs/content/guides/storage/quickstart.mdx +++ b/apps/docs/content/guides/storage/quickstart.mdx @@ -4,7 +4,7 @@ title: 'Storage Quickstart' description: 'Learn how to use Supabase to store and serve files.' subtitle: 'Learn how to use Supabase to store and serve files.' sidebar_label: 'Quickstart' -tocVideo: 'J9mTPY8rIXE' +tocVideo: ch2UGHMreB0 --- This guide shows the basic functionality of Supabase Storage. Find a full [example application on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management). diff --git a/apps/docs/content/guides/storage/schema/design.mdx b/apps/docs/content/guides/storage/schema/design.mdx index a7e1905a721..51818e324f2 100644 --- a/apps/docs/content/guides/storage/schema/design.mdx +++ b/apps/docs/content/guides/storage/schema/design.mdx @@ -4,6 +4,7 @@ title: 'The Storage Schema' description: 'Learn about the storage schema' subtitle: 'Learn about the storage schema' sidebar_label: 'Schema' +tocVideo: ch2UGHMreB0 --- Storage uses Postgres to store metadata regarding your buckets and objects. Users can use RLS (Row-Level Security) policies for access control. This data is stored in a dedicated schema within your project called `storage`. diff --git a/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx b/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx index fa251d50167..237d9516dcc 100644 --- a/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx +++ b/apps/docs/content/troubleshooting/error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr.mdx @@ -23,7 +23,7 @@ If you're not able to connect to the Supabase database and see one of the errors Is the server running on that host and accepting TCP/IP connections?` - `Circuit breaker open: Unable to establish connection to upstream database` -These bans will clear after 30mins but you can unban the IPs using the Supabase CLI https://supabase.com/docs/guides/cli following the commands below. +These bans clear after 30 minutes, but you can unban the IPs using the Supabase CLI. See [Local Development & CLI](/docs/guides/local-development) ## Unban IPs using the Dashboard diff --git a/apps/docs/content/troubleshooting/tls-handshake-failures-after-cbc-cipher-removal.mdx b/apps/docs/content/troubleshooting/tls-handshake-failures-after-cbc-cipher-removal.mdx new file mode 100644 index 00000000000..15e2d1c4aaf --- /dev/null +++ b/apps/docs/content/troubleshooting/tls-handshake-failures-after-cbc-cipher-removal.mdx @@ -0,0 +1,56 @@ +--- +title = "TLS handshake failures after removal of CBC-mode ciphers" +topics = [ "platform" ] +keywords = [ "tls", "ssl", "cipher", "cbc", "handshake", "https", "custom domain", "deprecation" ] + +[[errors]] +message = "sslv3 alert handshake failure" + +[[errors]] +message = "ERR_SSL_VERSION_OR_CIPHER_MISMATCH" + +[[errors]] +message = "The request was aborted: Could not create SSL/TLS secure channel." + +[[errors]] +message = "javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure" + +[[errors]] +message = "An SSL error has occurred and a secure connection to the server cannot be made." +--- + +Supabase will remove four cipher block chaining (CBC) mode cipher suites from TLS 1.2 on project APIs, on November 10, 2026 at the earliest. After the removal, clients that support only these ciphers can't connect to Auth, the Data API, Storage, Realtime, or Edge Functions. The removal applies to `.supabase.co` and to custom domains. + +Direct Postgres connections, connections through the pooler, and the Management API aren't affected. + +## Symptoms + +The client fails during the TLS handshake, before it sends any HTTP request, so the request doesn't appear in your project's API logs. Clients on up-to-date devices keep working. + +The error message depends on the client: + +| Client | Error message | +| ---------------------------------------------------- | --------------------------------------------------------------------------------- | +| OpenSSL-based clients, such as curl, Node.js, or PHP | `sslv3 alert handshake failure` | +| Browsers | `ERR_SSL_VERSION_OR_CIPHER_MISMATCH` | +| Windows PowerShell and .NET Framework | `The request was aborted: Could not create SSL/TLS secure channel.` | +| Java | `javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure` | +| Apple platforms | `An SSL error has occurred and a secure connection to the server cannot be made.` | + +## Cause + +The client can't use TLS 1.3, and the only TLS 1.2 cipher suites it shares with project APIs are the removed CBC-mode ones. This error usually means the client runs end-of-life software, such as an unsupported operating system or an embedded device that no longer receives updates. For the full list of removed and supported cipher suites, see [TLS for project APIs](/docs/guides/security/tls). + +If you see these errors before the removal date, the CBC-mode removal isn't the cause. For example, project APIs don't support TLS 1.0 or TLS 1.1, so clients limited to those versions can't connect at all. + +## Diagnose the client + +Check the TLS configuration your application actually uses: its operating system, runtime, and TLS library versions, and any TLS version or cipher suite settings in your code or configuration. A client that connects with TLS 1.3 isn't affected by the removal, whatever TLS 1.2 cipher suites it supports. + +## Fix the client + +Update the client's operating system, runtime, or TLS library to a version that supports TLS 1.3 or a supported TLS 1.2 cipher suite. + +If you can't update a client, route its traffic through a proxy that you control. The proxy accepts the client's ciphers and forwards requests to Supabase over a supported TLS connection. + +If you can't update the client or add a proxy, [open a support ticket](/dashboard/support/new). diff --git a/apps/docs/data/content-listings/getting-started.data.ts b/apps/docs/data/content-listings/getting-started.data.ts index a450b2186b7..ecca2f6e892 100644 --- a/apps/docs/data/content-listings/getting-started.data.ts +++ b/apps/docs/data/content-listings/getting-started.data.ts @@ -25,7 +25,7 @@ export const gettingStartedOverview: ContentListingGroup = { export const gettingStartedUseCases: ContentListingGroup = { id: 'getting-started-use-cases', heading: 'Use cases', - headingLevel: 'h3', + headingLevel: 'h2', type: 'grid', items: [ { @@ -56,7 +56,7 @@ export const gettingStartedUseCases: ContentListingGroup = { export const gettingStartedFrameworkQuickstarts: ContentListingGroup = { id: 'getting-started-framework-quickstarts', heading: 'Framework quickstarts', - headingLevel: 'h3', + headingLevel: 'h2', type: 'grid', items: [ { @@ -223,7 +223,7 @@ export const gettingStartedFrameworkQuickstarts: ContentListingGroup = { export const gettingStartedWebAppDemos: ContentListingGroup = { id: 'getting-started-web-app-demos', heading: 'Web app demos', - headingLevel: 'h3', + headingLevel: 'h2', type: 'grid', items: [ { @@ -304,7 +304,7 @@ export const gettingStartedWebAppDemos: ContentListingGroup = { export const gettingStartedMobileTutorials: ContentListingGroup = { id: 'getting-started-mobile-tutorials', heading: 'Mobile tutorials', - headingLevel: 'h3', + headingLevel: 'h2', type: 'grid', items: [ { diff --git a/apps/docs/docs/ref/csharp/release-notes.mdx b/apps/docs/docs/ref/csharp/release-notes.mdx index 2c1cc047c5d..ef2915683a7 100644 --- a/apps/docs/docs/ref/csharp/release-notes.mdx +++ b/apps/docs/docs/ref/csharp/release-notes.mdx @@ -5,43 +5,43 @@ title: Release Notes ## 8.1.0 - 2026-09-07 -- Stream Edge Function responses ([#417](https://github.com/supabase-community/supabase-csharp/issues/417)). -- Realtime: add an `enabled` flag to opt into the initial presence sync ([#407](https://github.com/supabase-community/supabase-csharp/issues/407)). -- Fix: keep `!` and unary `-` attached to their values in `Where` filter expressions ([#414](https://github.com/supabase-community/supabase-csharp/issues/414)). -- Fix: write integer arrays as JSON arrays and reject invalid literals with a `JsonException` ([#408](https://github.com/supabase-community/supabase-csharp/issues/408)). +- Stream Edge Function responses ([#417](https://github.com/supabase/supabase-csharp/issues/417)). +- Realtime: add an `enabled` flag to opt into the initial presence sync ([#407](https://github.com/supabase/supabase-csharp/issues/407)). +- Fix: keep `!` and unary `-` attached to their values in `Where` filter expressions ([#414](https://github.com/supabase/supabase-csharp/issues/414)). +- Fix: write integer arrays as JSON arrays and reject invalid literals with a `JsonException` ([#408](https://github.com/supabase/supabase-csharp/issues/408)). ## 8.0.0 - 2026-09-03 -Major release. All `Supabase.*` packages are versioned in lockstep. See the [migration guide](https://github.com/supabase-community/supabase-csharp/blob/master/docs/migrations/v8.0.0.md) for upgrade steps. +Major release. All `Supabase.*` packages are versioned in lockstep. See the [migration guide](https://github.com/supabase/supabase-csharp/blob/master/docs/migrations/v8.0.0.md) for upgrade steps. **Breaking changes** -- Migrate from `Newtonsoft.Json` to `System.Text.Json` across every package. Custom models with `[JsonProperty]` should move to `[JsonPropertyName]`; direct `JsonConvert` calls should move to `JsonSerializer` ([#360](https://github.com/supabase-community/supabase-csharp/issues/360)). +- Migrate from `Newtonsoft.Json` to `System.Text.Json` across every package. Custom models with `[JsonProperty]` should move to `[JsonPropertyName]`; direct `JsonConvert` calls should move to `JsonSerializer` ([#360](https://github.com/supabase/supabase-csharp/issues/360)). - Retarget every package to `netstandard2.1` (from `netstandard2.0`). .NET Framework and pre-`netstandard2.1` runtimes (Mono < 6.4, older Xamarin/Unity) are no longer supported — move to a `netstandard2.1`-capable target (.NET Core 3.0+/.NET 5+). -- Postgrest: parameterless `Table.Delete()` now returns `Task>` (the deleted rows) instead of `Task` ([#342](https://github.com/supabase-community/supabase-csharp/issues/342)). -- Postgrest: `Single()` now throws a `PostgrestException` (status `406`) when more than one row matches, instead of returning `null` ([#346](https://github.com/supabase-community/supabase-csharp/issues/346)). -- Realtime: registering a `postgres_changes` listener after `Subscribe()` now throws a `RealtimeException` ([#385](https://github.com/supabase-community/supabase-csharp/issues/385)). -- Gotrue: stop sending the OAuth `state` parameter to `/authorize`; `SignInOptions.State` and `ProviderAuthState.State` are removed ([#388](https://github.com/supabase-community/supabase-csharp/issues/388)). +- Postgrest: parameterless `Table.Delete()` now returns `Task>` (the deleted rows) instead of `Task` ([#342](https://github.com/supabase/supabase-csharp/issues/342)). +- Postgrest: `Single()` now throws a `PostgrestException` (status `406`) when more than one row matches, instead of returning `null` ([#346](https://github.com/supabase/supabase-csharp/issues/346)). +- Realtime: registering a `postgres_changes` listener after `Subscribe()` now throws a `RealtimeException` ([#385](https://github.com/supabase/supabase-csharp/issues/385)). +- Gotrue: stop sending the OAuth `state` parameter to `/authorize`; `SignInOptions.State` and `ProviderAuthState.State` are removed ([#388](https://github.com/supabase/supabase-csharp/issues/388)). - Gotrue: rename `NetworkStatus.PingCheck` to `PingCheckAsync`. **Features** -- Add the `Supabase.Extensions.DependencyInjection` package for DI registration ([#387](https://github.com/supabase-community/supabase-csharp/issues/387)). -- Add retry/backoff and injectable `HttpClient` support across all services ([#383](https://github.com/supabase-community/supabase-csharp/issues/383)). -- Support publishable and secret API keys ([#397](https://github.com/supabase-community/supabase-csharp/issues/397)). -- Gotrue: support async session persistence ([#399](https://github.com/supabase-community/supabase-csharp/issues/399)) and soft-delete on admin `DeleteUser` ([#402](https://github.com/supabase-community/supabase-csharp/issues/402)). -- Storage: expose the service error code ([#380](https://github.com/supabase-community/supabase-csharp/issues/380)). +- Add the `Supabase.Extensions.DependencyInjection` package for DI registration ([#387](https://github.com/supabase/supabase-csharp/issues/387)). +- Add retry/backoff and injectable `HttpClient` support across all services ([#383](https://github.com/supabase/supabase-csharp/issues/383)). +- Support publishable and secret API keys ([#397](https://github.com/supabase/supabase-csharp/issues/397)). +- Gotrue: support async session persistence ([#399](https://github.com/supabase/supabase-csharp/issues/399)) and soft-delete on admin `DeleteUser` ([#402](https://github.com/supabase/supabase-csharp/issues/402)). +- Storage: expose the service error code ([#380](https://github.com/supabase/supabase-csharp/issues/380)). **Fixes** -- Gotrue: a failed token refresh no longer signs the user out — only a server-reported invalid refresh token does ([#394](https://github.com/supabase-community/supabase-csharp/issues/394)). -- Postgrest: drop the `.` before nested `and`/`or` groups ([#389](https://github.com/supabase-community/supabase-csharp/issues/389)). -- Storage: percent-encode the object key in CDN purge URLs ([#384](https://github.com/supabase-community/supabase-csharp/issues/384)). +- Gotrue: a failed token refresh no longer signs the user out — only a server-reported invalid refresh token does ([#394](https://github.com/supabase/supabase-csharp/issues/394)). +- Postgrest: drop the `.` before nested `and`/`or` groups ([#389](https://github.com/supabase/supabase-csharp/issues/389)). +- Storage: percent-encode the object key in CDN purge URLs ([#384](https://github.com/supabase/supabase-csharp/issues/384)). ## 1.6.0 - 2026-08-07 -- Bump Supabase dependencies ([#301](https://github.com/supabase-community/supabase-csharp/issues/301)). -- Fix: match auth header names case-insensitively, enabling developer overrides ([#295](https://github.com/supabase-community/supabase-csharp/issues/295)). +- Bump Supabase dependencies ([#301](https://github.com/supabase/supabase-csharp/issues/301)). +- Fix: match auth header names case-insensitively, enabling developer overrides ([#295](https://github.com/supabase/supabase-csharp/issues/295)). ## 1.5.0 - 2026-07-30 @@ -57,7 +57,7 @@ Major release. All `Supabase.*` packages are versioned in lockstep. See the [mig This is the observability release: every child library now emits diagnostics through `System.Diagnostics`, making the SDK compatible with OpenTelemetry. -- Expose aggregated telemetry source names for OpenTelemetry ([#285](https://github.com/supabase-community/supabase-csharp/issues/285)). +- Expose aggregated telemetry source names for OpenTelemetry ([#285](https://github.com/supabase/supabase-csharp/issues/285)). - Update dependency: `Supabase.Core@1.2.0` - Add OpenTelemetry-compatible diagnostics primitives ([#6](https://github.com/supabase-community/core-csharp/issues/6)). - Update dependency: `Supabase.Gotrue@6.2.0` @@ -71,7 +71,7 @@ This is the observability release: every child library now emits diagnostics thr ## 1.3.0 - 2026-07-20 -- Wire Realtime's Postgrest client automatically so models received from `postgres_changes` support `Update()` and `Delete()` ([#282](https://github.com/supabase-community/supabase-csharp/issues/282)). +- Wire Realtime's Postgrest client automatically so models received from `postgres_changes` support `Update()` and `Delete()` ([#282](https://github.com/supabase/supabase-csharp/issues/282)). - Update dependency: `Supabase.Postgrest@4.3.0` - Add `Client.Attach()` to populate a model's client context for `Update`/`Delete` ([#135](https://github.com/supabase-community/postgrest-csharp/issues/135)). - Add `ClientOptions.SerializeEnumsAsStrings` to opt into string enum serialization ([#134](https://github.com/supabase-community/postgrest-csharp/issues/134)). @@ -81,7 +81,7 @@ This is the observability release: every child library now emits diagnostics thr ## 1.2.0 - 2026-07-16 -- Lower the `Newtonsoft.Json` minimum version to `13.0.2` across all packages to ease dependency resolution ([#275](https://github.com/supabase-community/supabase-csharp/issues/275)). +- Lower the `Newtonsoft.Json` minimum version to `13.0.2` across all packages to ease dependency resolution ([#275](https://github.com/supabase/supabase-csharp/issues/275)). - Update dependency: `Supabase.Gotrue@6.1.0` - Add an option for setting `redirect_url` on MagicLink sign-in. - Add `state` parameter support to OAuth provider sign-in. @@ -117,7 +117,7 @@ This is the observability release: every child library now emits diagnostics thr - Update dependency: `Supabase.Realtime@7.0.2` - Updates dependency: `Websocket.Client@5.1.2`. - Updates dependency: `Supabase.Postgrest@4.0.3`. - - Adds support for specifying `GetHeaders` on the `RealtimeClient`, which are included on the initial request to establish the websocket connection ([#167](https://github.com/supabase-community/supabase-csharp/issues/167)). + - Adds support for specifying `GetHeaders` on the `RealtimeClient`, which are included on the initial request to establish the websocket connection ([#167](https://github.com/supabase/supabase-csharp/issues/167)). ## 1.1.0 - 2024-07-25 @@ -170,10 +170,10 @@ This is the observability release: every child library now emits diagnostics thr - Assembly Name has been changed to `Supabase.dll` - Update dependency: `postgrest-csharp@5.0.0` - [MAJOR] Moves namespaces from `Postgrest` to `Supabase.Postgrest` - - Re: [#135](https://github.com/supabase-community/supabase-csharp/issues/135) Update nuget package + - Re: [#135](https://github.com/supabase/supabase-csharp/issues/135) Update nuget package name `postgrest-csharp` to `Supabase.Postgrest` - Update dependency: `gotrue-csharp@5.0.0` - - Re: [#135](supabase-community/supabase-csharp#135) Update nuget package name `gotrue-csharp` to `Supabase.Gotrue` + - Re: [#135](supabase/supabase-csharp#135) Update nuget package name `gotrue-csharp` to `Supabase.Gotrue` - Re: [#89](https://github.com/supabase-community/gotrue-csharp/issues/89), Only add `access_token` to request body when it is explicitly declared. - [MINOR] Re: [#89](https://github.com/supabase-community/gotrue-csharp/issues/89) Update signature @@ -185,16 +185,16 @@ This is the observability release: every child library now emits diagnostics thr - Update dependency: `realtime-csharp@7.0.0` - Merges [#45](https://github.com/supabase-community/realtime-csharp/pull/45) - Updating the `Websocket.Client@5.1.1` - - Re: [#135](https://github.com/supabase-community/supabase-csharp/issues/135) Update nuget package + - Re: [#135](https://github.com/supabase/supabase-csharp/issues/135) Update nuget package name `realtime-csharp` to `Supabase.Realtime` - Update dependency: `storage-csharp@2.0.0` - - Re: [#135](https://github.com/supabase-community/supabase-csharp/issues/135) Update nuget package + - Re: [#135](https://github.com/supabase/supabase-csharp/issues/135) Update nuget package name `storage-csharp` to `Supabase.Storage` - Update dependency: `functions-csharp@2.0.0` - - Re: [#135](https://github.com/supabase-community/supabase-csharp/issues/135) Update nuget package + - Re: [#135](https://github.com/supabase/supabase-csharp/issues/135) Update nuget package name `functions-csharp` to `Supabase.Functions` - Update dependency: `core-csharp@1.0.0` - - Re: [#135](https://github.com/supabase-community/supabase-csharp/issues/135) Update nuget package + - Re: [#135](https://github.com/supabase/supabase-csharp/issues/135) Update nuget package name `supabase-core` to `Supabase.Core` - Adds comments to the remaining undocumented code. diff --git a/apps/docs/docs/ref/csharp/v0/release-notes.mdx b/apps/docs/docs/ref/csharp/v0/release-notes.mdx index df2d78c8fdf..a594b442caf 100644 --- a/apps/docs/docs/ref/csharp/v0/release-notes.mdx +++ b/apps/docs/docs/ref/csharp/v0/release-notes.mdx @@ -13,7 +13,7 @@ title: Release Notes ## 0.16.1 - 2024-03-15 - Update dependency: `postgrest-csharp@3.5.1` - - Re: [#147](https://github.com/supabase-community/supabase-csharp/issues/147) - Supports `Rpc` specifying a generic + - Re: [#147](https://github.com/supabase/supabase-csharp/issues/147) - Supports `Rpc` specifying a generic type for its return. ## 0.16.0 - 2024-03-12 @@ -77,9 +77,9 @@ title: Release Notes ## 0.13.5 - 2023-10-09 - Update Dependency: `postgrest-csharp@3.2.9` - - Re: [supabase-csharp#115](https://github.com/supabase-community/supabase-csharp/discussions/115) Additional + - Re: [supabase-csharp#115](https://github.com/supabase/supabase-csharp/discussions/115) Additional support for a model referencing another model with multiple foreign keys. - - Re: [supabase-csharp#115](https://github.com/supabase-community/supabase-csharp/discussions/115) Adds support for + - Re: [supabase-csharp#115](https://github.com/supabase/supabase-csharp/discussions/115) Adds support for multiple references attached to the same model (foreign keys) on a single C# Model. ## 0.13.4 - 2023-10-08 @@ -90,7 +90,7 @@ title: Release Notes ## 0.13.3 - 2023-09-15 -- Re: [#107](https://github.com/supabase-community/supabase-csharp/issues/107) - removes Realtime socket being +- Re: [#107](https://github.com/supabase/supabase-csharp/issues/107) - removes Realtime socket being disconnected on a User sign-out - only the subscriptions should be removed. ## 0.13.2 - 2023-09-15 @@ -98,7 +98,7 @@ title: Release Notes - Update dependency: `postgrest-csharp@3.2.7` - Implements a `TableWithCache` for `Get` requests that can pull reactive Models from cache before making a remote request. - - Re: [supabase-csharp#85](https://github.com/supabase-community/supabase-csharp/issues/85) Includes sourcelink + - Re: [supabase-csharp#85](https://github.com/supabase/supabase-csharp/issues/85) Includes sourcelink support. - Re: [#75](https://github.com/supabase-community/postgrest-csharp/pull/75) Fix issue with marshalling of stored procedure arguments. Big thank you to [@corrideat](https://github.com/corrideat)! @@ -131,7 +131,7 @@ title: Release Notes disconnect from channels after a few hours and fail to reconnect by removing the case where the `IsSubscribe` flag is flipped when encountering a channel error. - Update dependency: `postgrest-csharp@3.2.5` - - Re: [supabase-community/supabase-csharp#81](https://github.com/supabase-community/supabase-csharp/discussions/81): + - Re: [supabase/supabase-csharp#81](https://github.com/supabase/supabase-csharp/discussions/81): Clarifies `ReferenceAttribute` by changing `shouldFilterTopLevel` to `useInnerJoin` and adds an additional constructor for `ReferenceAttribute` with a shortcut for specifying the `JoinType` @@ -318,7 +318,7 @@ Thanks [@wiverson](https://github.com/wiverson) for the work in this release! - Update dependency: supabase-storage-csharp@1.2.5 - Provides fix - for [supabase-community/supabase-csharp#54](https://github.com/supabase-community/supabase-csharp/issues/54) - + for [supabase/supabase-csharp#54](https://github.com/supabase/supabase-csharp/issues/54) - Dynamic headers were always being overwritten by initialized token headers, so the storage client would not receive user's access token as expected. - Provides fix for upload progress not reporting @@ -376,7 +376,7 @@ Thanks [@wiverson](https://github.com/wiverson) for the work in this release! ## 0.7.1 - 2023-01-17 - Update dependency: postgrest-csharp@3.1.1 - - Fix issue from supabase-community/supabase-csharp#48 where boolean model properties would not be evaluated in + - Fix issue from supabase/supabase-csharp#48 where boolean model properties would not be evaluated in predicate expressions ## 0.7.0 - 2023-01-16 @@ -392,7 +392,7 @@ Thanks [@wiverson](https://github.com/wiverson) for the work in this release! - Update dependency: postgrest-csharp@3.0.4 - `GetHeaders` is now passed to `ModeledResponse` and `BaseModel` so that the default `Update` and `Delete` methods use the latest credentials - - `GetHeaders` is used in `Rpc` calls (re: [#39](https://github.com/supabase-community/supabase-csharp/issues/39)) + - `GetHeaders` is used in `Rpc` calls (re: [#39](https://github.com/supabase/supabase-csharp/issues/39)) ## 0.6.1 - 2022-11-12 @@ -427,7 +427,7 @@ Other Changes: Big thank you to [@veleek](https://github.com/veleek) for his insight into these changes. -Re: [#35](https://github.com/supabase-community/supabase-csharp/issues/35), [#34](https://github.com/supabase-community/supabase-csharp/issues/34), [#23](https://github.com/supabase-community/supabase-csharp/issues/23), [#36](https://github.com/supabase-community/supabase-csharp/pull/36) +Re: [#35](https://github.com/supabase/supabase-csharp/issues/35), [#34](https://github.com/supabase/supabase-csharp/issues/34), [#23](https://github.com/supabase/supabase-csharp/issues/23), [#36](https://github.com/supabase/supabase-csharp/pull/36) ## 0.5.3 - 2022-10-11 @@ -534,10 +534,10 @@ Re: [#35](https://github.com/supabase-community/supabase-csharp/issues/35), [#34 ## 0.2.11 - 2021-12-24 - Update dependency: gotrue-csharp@2.3.2 (changes CreateUser parameters to conform to `AdminUserAttributes`) - - See [#15](https://github.com/supabase-community/supabase-csharp/issues/15) - - See [#16](https://github.com/supabase-community/supabase-csharp/issues/16) + - See [#15](https://github.com/supabase/supabase-csharp/issues/15) + - See [#16](https://github.com/supabase/supabase-csharp/issues/16) - Update dependency: realtime-csharp@2.0.7 - - See [#13](https://github.com/supabase-community/supabase-csharp/issues/13) + - See [#13](https://github.com/supabase/supabase-csharp/issues/13) ## 0.2.10 - 2021-12-23 @@ -560,7 +560,7 @@ Re: [#35](https://github.com/supabase-community/supabase-csharp/issues/35), [#34 ## 0.2.6 - 2021-11-29 -- Support for [#12](https://github.com/supabase-community/supabase-csharp/issues/12) +- Support for [#12](https://github.com/supabase/supabase-csharp/issues/12) - Update realtime-csharp to 2.0.6 - Update gotrue-csharp to 2.2.2 -- Add `StatelessClient` re:[#7](https://github.com/supabase-community/supabase-csharp/issues/7) +- Add `StatelessClient` re:[#7](https://github.com/supabase/supabase-csharp/issues/7) diff --git a/apps/docs/docs/ref/middleware/introduction.mdx b/apps/docs/docs/ref/middleware/introduction.mdx index ba9b97cfff0..6d235bb6e7a 100644 --- a/apps/docs/docs/ref/middleware/introduction.mdx +++ b/apps/docs/docs/ref/middleware/introduction.mdx @@ -7,12 +7,6 @@ title: Introduction Each middleware can guard the request, edit the response, or contribute typed values to a shared context that later middleware and your handler read. TypeScript checks the composition: a middleware that needs a value from an earlier middleware will not compile unless that middleware runs first. - - -`@supabase/middleware` is in alpha. APIs may change between 0.x releases. - - - This package contains the engine and the framework-neutral middleware: `withCors` and `withFeatureFlag`. Supabase-specific middleware such as `withClaims`, `withSupabaseClient`, `withSupabaseAdminClient`, `withPostgresClient`, and `withPostgresAdminClient` ships in [`@supabase/server`](/docs/reference/server/introduction). ### Trust model diff --git a/apps/docs/docs/ref/middleware/usage-examples.mdx b/apps/docs/docs/ref/middleware/usage-examples.mdx index 4a557683d32..d302ff4b4b0 100644 --- a/apps/docs/docs/ref/middleware/usage-examples.mdx +++ b/apps/docs/docs/ref/middleware/usage-examples.mdx @@ -53,7 +53,7 @@ Each example builds one Fetch handler with `pipeline`. Entries run in array orde Without `withCors`, `withSupabase` answers every `OPTIONS` request itself with `204` and wildcard CORS headers (`Access-Control-Allow-Origin: *`). That is enough when you do not need an origin allowlist. A layer that owns CORS must sit before `withSupabase` in the array. Placed after it, the preflight reaches the auth gate and gets a `401`. - The entry form of `withSupabase` is alpha. It needs `@supabase/server` 1.6.0 or later. + The entry form of `withSupabase` needs `@supabase/server` 1.6.0 or later. diff --git a/apps/docs/docs/ref/server/frameworks.mdx b/apps/docs/docs/ref/server/frameworks.mdx index 46260d7dc8d..49fdcf7dee9 100644 --- a/apps/docs/docs/ref/server/frameworks.mdx +++ b/apps/docs/docs/ref/server/frameworks.mdx @@ -5,12 +5,14 @@ title: Frameworks `@supabase/server` middleware runs inside Hono, H3, Elysia, NestJS, and TanStack Start through a bridge: one file you copy into your project. The bridge runs a `@supabase/middleware` entry array in the framework's own middleware slot. Your route handlers then read `supabase`, `jwtClaims`, and any other key the entries contribute from the place your framework keeps per-request state. -The framework adapters (`@supabase/server/adapters/*`) predate the middleware engine. We are moving off them, and they will be deprecated soon. The bridges on this page replace them. If you use an adapter today, read "Moving off the adapters" below before you change anything. One step in that migration can open an endpoint up without an error. +The framework adapters (`@supabase/server/adapters/*`) predate the middleware engine. They are deprecated and will be removed on December 1, 2026. The bridges on this page replace them. If you use an adapter today, read [Moving off the adapters](/docs/reference/server/frameworks#moving-off-the-adapters) before you change anything. One step in that migration can open an endpoint up without an error. The bridges need `@supabase/server` 1.6.0 or later and Node 22 or later. `withRequiredClaims` shipped in 1.5.1 and the entry form of `withSupabase` in 1.6.0. Releases before 1.5.1 have no `/middleware/*` exports. +The bridge file imports `@supabase/middleware`, so add it to your project as a direct dependency. `@supabase/server` depends on it, but a strict package manager such as pnpm does not let your code import a transitive dependency. + ### What a bridge gives you @@ -268,7 +270,7 @@ The bridge files live in the server repository under [`examples/frameworks`](htt Each adapter solved the same problem in a framework-specific way: build a `SupabaseContext` and stash it where the handler can reach it. That meant a published entry point, a peer dependency range, and a release cycle per framework, all to wrap one call. The engine replaced the model. A middleware is now an entry, a `(handler) => handler` wrapper over the Web `Request` and `Response` pair, and `withSupabaseClient()`, `withSupabaseAdminClient()`, `withRequiredClaims()`, and the rest are ordinary entries you compose. What remains is the bridge, and it belongs in your project, not in a package that has to track your framework's major versions. -The adapters still work in v1 and will be deprecated soon. They will be removed in a future major. `withSupabase(config, handler)` from `@supabase/server` is not part of this change. +The adapters are deprecated. They still work today and will be removed on December 1, 2026. `withSupabase(config, handler)` from `@supabase/server` is not part of this change. Three changes come with the migration. The first can open an endpoint up. @@ -329,7 +331,7 @@ Neither entry answers a CORS preflight, and the short-circuits carry no CORS hea `withSupabase(config, handler)` still does the verify-then-reject work for you, and it is not deprecated. It wraps a fetch handler rather than composing into a framework chain, so it does not slot into a bridge. If an endpoint is already a plain fetch handler, staying on `withSupabase()` is a legitimate end state. It is also the only way to get the full `SupabaseContext`, `userClaims` and `authMode` included, behind an auth gate. -To compose other entries around it, `withSupabase({ auth: 'user' })` with no handler is a `pipeline` entry placed by position. Entries before it run ahead of the auth gate. Entries after it receive the full `SupabaseContext`. Nesting, as in `withSupabase(config, entry(handler))`, still works. The entry form is alpha and needs 1.6.0 or later. +To compose other entries around it, `withSupabase({ auth: 'user' })` with no handler is a `pipeline` entry placed by position. Entries before it run ahead of the auth gate. Entries after it receive the full `SupabaseContext`. Nesting, as in `withSupabase(config, entry(handler))`, still works. The entry form needs 1.6.0 or later. ```ts import { pipeline } from '@supabase/middleware' @@ -458,6 +460,7 @@ Do the steps in order. The auth inventory comes before any code change, because ```bash + npm install @supabase/middleware mkdir -p src/lib curl --fail -o src/lib/supabase-middleware.ts \ https://raw.githubusercontent.com/supabase/server/main/examples/frameworks/hono/supabase-middleware.ts @@ -614,7 +617,8 @@ contributes jwtClaims: null and falls through, turning a 401 endpoint into a auth: 'user'. Never compose withClaims() and withRequiredClaims() together; they share the jwtClaims key and that is a compile-time conflict. -STEP 3: FETCH THE BRIDGE. Download +STEP 3: FETCH THE BRIDGE. Add @supabase/middleware as a direct dependency of +the project; the bridge imports it. Then download https://raw.githubusercontent.com/supabase/server/main/examples/frameworks/hono/supabase-middleware.ts and save it as src/lib/supabase-middleware.ts, verbatim. Do not "improve", condense, or drop comments from it. diff --git a/apps/docs/features/SearchV2/SearchV2Dialog.tsx b/apps/docs/features/SearchV2/SearchV2Dialog.tsx index e0ffd7f79c7..dba642f0f86 100644 --- a/apps/docs/features/SearchV2/SearchV2Dialog.tsx +++ b/apps/docs/features/SearchV2/SearchV2Dialog.tsx @@ -82,7 +82,7 @@ function SearchV2Panel({ onResultSelect, onResultOpen }: SearchV2PanelProps) { if (searchState.status === 'results' || searchState.status === 'noResults') { sendTelemetryEvent({ action: 'docs_search_v2_search_submitted', - properties: { query: searchState.query }, + properties: { query: searchState.query, hasResults: searchState.status === 'results' }, }) } }, [searchState, sendTelemetryEvent]) diff --git a/apps/docs/features/docs/Troubleshooting.script.mjs b/apps/docs/features/docs/Troubleshooting.script.mjs deleted file mode 100644 index ff6e9eda6c7..00000000000 --- a/apps/docs/features/docs/Troubleshooting.script.mjs +++ /dev/null @@ -1,502 +0,0 @@ -// @ts-check - -/* eslint-disable turbo/no-undeclared-env-vars */ - -/** - * Sync new troubleshooting entries from the GitHub repo with GitHub - * Discussions. - */ - -import '../../scripts/utils/dotenv.js' - -import { createAppAuth } from '@octokit/auth-app' -import { Octokit } from '@octokit/core' -import { createClient } from '@supabase/supabase-js' -import { createHash } from 'crypto' -import matter from 'gray-matter' -import { fromMarkdown } from 'mdast-util-from-markdown' -import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm' -import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx' -import { toMarkdown } from 'mdast-util-to-markdown' -import { gfm } from 'micromark-extension-gfm' -import { mdxjs } from 'micromark-extension-mdxjs' -import { readFile, writeFile } from 'node:fs/promises' -import { parse, stringify } from 'smol-toml' - -import { - getAllTroubleshootingEntriesInternal as getAllTroubleshootingEntries, - getArticleSlug, -} from './Troubleshooting.utils.common.mjs' - -const REPOSITORY_ID = 'MDEwOlJlcG9zaXRvcnkyMTQ1ODcxOTM=' -const TROUBLESHOOTING_CATEGORY_ID = 'DIC_kwDODMpXOc4CUvEr' -const REPOSITORY_OWNER = 'supabase' -const REPOSITORY_NAME = 'supabase' - -/** - * @typedef {import('./Troubleshooting.utils.common.mjs').TroubleshootingEntry} TroubleshootingEntry - * @typedef {import('./Troubleshooting.utils.common.mjs').TroubleshootingMetadata} TroubleshootingMetadata - */ - -/** - * @type {import('@octokit/core').Octokit} - */ -let octokitInstance -/** - * @type {import('@supabase/supabase-js').SupabaseClient} - */ -let supabaseAdminClient - -function octokit() { - if (!octokitInstance) { - octokitInstance = new Octokit({ - authStrategy: createAppAuth, - auth: { - appId: process.env.DOCS_GITHUB_APP_ID, - installationId: process.env.DOCS_GITHUB_APP_INSTALLATION_ID, - privateKey: process.env.DOCS_GITHUB_APP_PRIVATE_KEY, - }, - }) - } - - return octokitInstance -} - -export function supabaseAdmin() { - if (!supabaseAdminClient) { - supabaseAdminClient = createClient( - /** @type {string} */ (process.env.NEXT_PUBLIC_SUPABASE_URL), - /** @type {string} */ (process.env.SUPABASE_SECRET_KEY) - ) - } - - return supabaseAdminClient -} - -async function syncTroubleshootingEntries() { - console.log('[START] Syncing troubleshooting entries to GitHub') - - const [troubleshootingEntries, discussions] = await Promise.all([ - getAllTroubleshootingEntries(), - getAllTroubleshootingDiscussions(), - ]) - - console.log(`[INFO] Found ${troubleshootingEntries.length} existing entries`) - - const tasks = troubleshootingEntries.map(async (entry) => { - const databaseId = entry.data.database_id - if (databaseId.startsWith('pseudo-')) { - // The database entry is faked, so we may need to create a new one. - // There's also an edge case we need to check for: the entry has already - // been created, but the new database ID hasn't been written to the file - // yet. - if (await entryExists(entry)) return - - const discussion = entry.data.github_url - ? await getGithubIdForDiscussion(discussions, entry) - : await createGithubDiscussion(entry) - - let id - try { - id = await insertNewTroubleshootingEntry(entry, discussion) - } catch (error) { - console.error(`[ERROR] Failed to insert new entry for ${entry.data.title}: %O`, error) - console.log( - `[INFO] Rolling back discussion creation for ${entry.data.title} (GitHub ID ${discussion.id})` - ) - await rollbackGithubDiscussion(discussion.id) - throw error - } - - await updateFileId(entry, id) - } else { - // The database entry already exists, so check for updates. - const contentHasChanged = await updateChecksumIfNeeded(entry) - if (contentHasChanged) { - await updateGithubDiscussion(entry) - } - } - }) - - const results = await Promise.allSettled(tasks) - let hasErrors = false - results.forEach((result, index) => { - if (result.status === 'rejected') { - console.error( - `[ERROR] Failed to insert and/or update for ${troubleshootingEntries[index].filePath}:\n%O`, - result.reason?.errors ?? result.reason - ) - hasErrors = true - } - }) - - return hasErrors -} - -/** - * @param {TroubleshootingEntry} entry - * @throws If the database check fails for some unknown reason - */ -async function entryExists(entry) { - const checksum = calculateChecksum(entry.content) - const { error } = await supabaseAdmin() - .from('troubleshooting_entries') - .select('id') - .eq('checksum', checksum) - .single() - - if (error) { - if (error.code === 'PGRST116') { - // No entry found - return false - } - throw error - } - - console.log( - `[INFO] Entry for ${entry.data.title} already exists. Not creating a new one to prevent duplicates.` - ) - return true -} - -/** - * @param {string} content - */ -function calculateChecksum(content) { - // Normalize to ignore changes that don't affect the final displayed content. - const mdast = fromMarkdown(content, { - extensions: [gfm(), mdxjs()], - mdastExtensions: [gfmFromMarkdown(), mdxFromMarkdown()], - }) - const bodyNormalized = toMarkdown(mdast, { extensions: [gfmToMarkdown(), mdxToMarkdown()] }) - - const { data, content: body } = matter(bodyNormalized, { - language: 'toml', - engines: { toml: parse }, - }) - const newFrontmatter = stringify(data) - const normalized = `---\n${newFrontmatter}\n---\n${body}` - - return createHash('sha256').update(normalized).digest('base64') -} - -/** - * @param {TroubleshootingEntry} entry - * - * @param {Object} discussion - * @param {string} discussion.id - * @param {string} discussion.url - * - * @throws If the database insertion fails - */ -async function insertNewTroubleshootingEntry(entry, discussion) { - console.log(`[INFO] Inserting entry for ${entry.data.title} into DB`) - - const now = new Date().toISOString() - const checksum = calculateChecksum(entry.content) - - const { data, error } = await supabaseAdmin() - .from('troubleshooting_entries') - .insert({ - api: entry.data.api, - checksum, - date_created: entry.data.date_created?.toISOString() ?? now, - date_updated: now, - errors: entry.data.errors, - github_id: discussion.id, - github_url: discussion.url, - keywords: entry.data.keywords, - title: entry.data.title, - topics: entry.data.topics, - }) - .select('id') - .single() - if (error) { - throw error - } - - return data.id -} - -/** - * @param {TroubleshootingEntry} entry - * - * @throws If the database update fails - */ -async function updateChecksumIfNeeded(entry) { - const { data, error } = await supabaseAdmin() - .from('troubleshooting_entries') - .select('checksum') - .eq('id', entry.data.database_id) - .single() - if (error) { - throw error - } - - const newChecksum = calculateChecksum(entry.content) - if (data.checksum !== newChecksum) { - console.log(`[INFO] Content changed for ${entry.data.title}. Updating checksum.`) - - const now = new Date().toISOString() - const { error } = await supabaseAdmin() - .from('troubleshooting_entries') - .update({ - checksum: newChecksum, - date_updated: now, - }) - .eq('id', entry.data.database_id) - - if (error) { - throw error - } - - return true - } - - return false -} - -/** - * Converts relative links to absolute URLs for GitHub discussions using MDAST - * @param {string} content - The markdown content to process (already stripped of JSX) - */ -function rewriteRelativeLinks(content) { - const baseUrl = 'https://supabase.com' - - // Parse the markdown to AST - const mdast = fromMarkdown(content, { - extensions: [gfm()], - mdastExtensions: [gfmFromMarkdown()], - }) - - // Walk the tree and modify link nodes - /** - * @param {import('mdast').Root|import('mdast').Content} node - */ - function visitNode(node) { - if (node.type === 'link' && node.url && node.url.startsWith('/')) { - // Convert relative URL to absolute - node.url = `${baseUrl}${node.url}` - } - - // Recursively visit children - if ('children' in node) { - node.children.forEach(visitNode) - } - } - - visitNode(mdast) - - // Convert back to markdown - return toMarkdown(mdast, { extensions: [gfmToMarkdown()] }) -} - -/** - * @param {TroubleshootingEntry} entry - */ -function addCanonicalUrl(entry) { - const docsUrl = 'https://supabase.com/docs/guides/troubleshooting/' + getArticleSlug(entry) - const contentWithAbsoluteLinks = rewriteRelativeLinks(entry.contentWithoutJsx) - const content = - `_This is a copy of a troubleshooting article on Supabase's docs site. It may be missing some details from the original. View the [original article](${docsUrl})._\n\n` + - contentWithAbsoluteLinks - return content -} - -/** - * @param {TroubleshootingEntry} entry - */ -async function createGithubDiscussion(entry) { - console.log(`[INFO] Creating GitHub discussion for ${entry.data.title}`) - const content = addCanonicalUrl(entry) - - const mutation = ` - mutation CreateDiscussionMutation( - $repository: ID!, - $category: ID!, - $title: String!, - $body: String! - ) { - createDiscussion(input: { - repositoryId: $repository, - categoryId: $category, - body: $body, - title: $title - }) { - discussion { - id - url - } - } - } - ` - - const { - createDiscussion: { discussion }, - } = await octokit().graphql(mutation, { - repository: REPOSITORY_ID, - category: TROUBLESHOOTING_CATEGORY_ID, - body: content, - title: entry.data.title, - }) - console.log(`[INFO] Created GitHub discussion for ${entry.data.title}: %s`, discussion.url) - return discussion -} - -/** - * @returns {Promise<{id: string, url: string}[]>} - */ -async function getAllTroubleshootingDiscussions() { - const query = ` - query getDiscussions($cursor: String) { - repository(owner: "${REPOSITORY_OWNER}", name: "${REPOSITORY_NAME}") { - discussions(first: 100, after: $cursor, categoryId: "${TROUBLESHOOTING_CATEGORY_ID}") { - pageInfo { - hasNextPage - endCursor - } - nodes { - id - url - } - } - } - } - ` - - const discussions = [] - let hasNextPage = true - let cursor - - while (hasNextPage) { - const { - repository: { - discussions: { nodes: moreDiscussions, pageInfo }, - }, - } = await octokit().graphql(query, { cursor }) - - discussions.push(...moreDiscussions) - hasNextPage = pageInfo.hasNextPage - cursor = pageInfo.endCursor - } - - return discussions -} - -/** - * @param {{id: string, url: string}[]} discussions - * @param {TroubleshootingEntry} entry - * - * @throws If matching discussion not found - */ -async function getGithubIdForDiscussion(discussions, entry) { - const matchingDiscussion = discussions.find( - (discussion) => discussion.url === entry.data.github_url - ) - if (!matchingDiscussion) { - throw new Error(`No matching discussion found for URL: ${entry.data.github_url}`) - } - return matchingDiscussion -} - -/** - * @param {TroubleshootingEntry} entry - * - * @throws If stored discussion ID for entry not found - */ -async function updateGithubDiscussion(entry) { - console.log(`[INFO] Updating discussion content for ${entry.data.title}`) - - const { data, error } = await supabaseAdmin() - .from('troubleshooting_entries') - .select('github_id') - .eq('id', entry.data.database_id) - .single() - if (error) { - throw error - } - - const content = addCanonicalUrl(entry) - const mutation = ` - mutation UpdateDiscussionMutation( - $discussionId: ID!, - $body: String! - ) { - updateDiscussion(input: { - discussionId: $discussionId, - body: $body - }) { - discussion { - id - } - } - } - ` - try { - await octokit().graphql(mutation, { discussionId: data.github_id, body: content }) - console.log(`[INFO] Updated discussion content for ${entry.data.title}`) - } catch (err) { - console.error('[DEBUG] Failed GraphQL mutation:\n', mutation) - throw err - } -} - -/** @param {string} id */ -async function rollbackGithubDiscussion(id) { - try { - const mutation = ` - mutation DeleteDiscussionMutation( - $discussionId: ID! - ) { - deleteDiscussion(input: { - discussionId: $discussionId, - }) { - discussion { - id - } - } - } - ` - - await octokit().graphql(mutation, { discussionId: id }) - console.log(`[INFO] Rolled back discussion creation for ${id}`) - } catch (error) { - console.error(`[ERROR] Failed to rollback discussion creation for ${id}: %O`, error) - } -} - -/** - * @param {TroubleshootingEntry} entry - * @param {string} id - * - * @throws Passes through readFile and writeFile errors without catching - */ -async function updateFileId(entry, id) { - console.log(`[INFO] Writing database ID to file for ${entry.filePath}`) - - const fileContents = await readFile(entry.filePath, 'utf-8') - const { data, content } = matter(fileContents, { - language: 'toml', - engines: { toml: parse }, - }) - data.database_id = id - - const newFrontmatter = stringify(data) - const newContent = `---\n${newFrontmatter}\n---\n${content}` - - await writeFile(entry.filePath, newContent) -} - -async function main() { - try { - const hasErrors = await syncTroubleshootingEntries() - if (hasErrors) { - process.exit(1) - } - } catch (error) { - console.error(`[ERROR] %O`, error) - process.exit(1) - } -} - -main() diff --git a/apps/docs/features/docs/Troubleshooting.utils.common.mjs b/apps/docs/features/docs/Troubleshooting.utils.common.mjs index de3e1028e48..21b35392026 100644 --- a/apps/docs/features/docs/Troubleshooting.utils.common.mjs +++ b/apps/docs/features/docs/Troubleshooting.utils.common.mjs @@ -1,11 +1,10 @@ // @ts-check /** - * This file is for utils needed in both the Next.js app build and the - * troubleshooting sync script. Because of unsolved problems with imports, the - * script is a mjs file instead of a ts file. Any dependencies that are needed - * in both places are defined here, and then typed in Troubleshooting.utils.ts - * as required. + * This file is for utils needed in both the Next.js app build and other + * scripts that read troubleshooting entries (e.g. search embeddings + * generation). Any dependencies that are needed in both places are defined + * here, and then typed in Troubleshooting.utils.ts as required. */ import matter from 'gray-matter' diff --git a/apps/docs/lib/content-listings.test.ts b/apps/docs/lib/content-listings.test.ts index 1eeb4059884..2b0a995cc52 100644 --- a/apps/docs/lib/content-listings.test.ts +++ b/apps/docs/lib/content-listings.test.ts @@ -257,6 +257,17 @@ describe('serializeContentListingGroupToMarkdown', () => { }) describe('ContentListings markdown handler', () => { + it.each([ + 'getting-started-use-cases', + 'getting-started-framework-quickstarts', + 'getting-started-web-app-demos', + 'getting-started-mobile-tutorials', + ])('renders %s as a second-level section under the page title', (id) => { + const markdown = ContentListingsMarkdownHandler({ props: { id } }) + + expect(markdown).toMatch(/^## [^\n]+\n/) + }) + it('serializes the group looked up by id', () => { const markdown = ContentListingsMarkdownHandler({ props: { id: 'storage-get-started' } }) diff --git a/apps/docs/package.json b/apps/docs/package.json index c0c615f6dff..fe7fb8fd45b 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -22,10 +22,8 @@ "codegen:references:new": "pnpm run codegen:references:new:ensure && tsx scripts/build-reference-content.ts", "codegen:references:legacy": "tsx features/docs/Reference.generated.script.ts", "codegen:references": "pnpm codegen:references:legacy && pnpm codegen:references:new", - "dev": "run-p --race dev:next dev:watch:troubleshooting", - "dev:next": "next dev --port 3001", + "dev": "next dev --port 3001", "dev:secrets:pull": "AWS_PROFILE=supa-dev node ../../scripts/getSecrets.js -n local/docs", - "dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs", "embeddings": "tsx --conditions=react-server scripts/search/generate-embeddings.ts", "embeddings:nimbus": "ENABLED_FEATURES_OVERRIDE_DISABLE_ALL=true pnpm run embeddings", "embeddings:refresh": "pnpm run embeddings --refresh", @@ -50,7 +48,6 @@ "test:local": "vitest --exclude \"**/*.smoke.test.ts\"", "test:local:unwatch": "vitest --exclude \"**/*.smoke.test.ts\" --run", "test:smoke": "pnpm run codegen:references && vitest -t \"prod smoke test\"", - "troubleshooting:sync": "node features/docs/Troubleshooting.script.mjs", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -63,7 +60,6 @@ "@octokit/auth-app": "^7.0.0", "@octokit/core": "^7.0.6", "@octokit/graphql": "^8.0.0", - "@octokit/plugin-paginate-graphql": "^4.0.0", "@octokit/plugin-retry": "^8.1.0", "@sentry/nextjs": "catalog:", "@supabase/supabase-js": "catalog:", @@ -78,7 +74,6 @@ "config": "workspace:*", "dev-tools": "workspace:*", "eslint-config-supabase": "workspace:*", - "framer-motion": "^11.18.2", "github-slugger": "^2.0.0", "graphql": "^16.10.0", "graphql-validation-complexity": "^0.4.2", @@ -103,6 +98,7 @@ "mdast-util-to-string": "^3.1.1", "micromark-extension-gfm": "^2.0.3", "micromark-extension-mdxjs": "^1.0.0", + "motion": "catalog:", "next": "^15.5.24", "next-mdx-remote-client": "^1.1.7", "next-plugin-yaml": "^1.0.1", @@ -161,19 +157,17 @@ "amaro": "^1.1.5", "api-types": "workspace:*", "cheerio": "^1.0.0-rc.12", - "chokidar": "^4.0.3", "config": "workspace:*", "dotenv": "^16.0.3", "ejs": "^3.1.10", "fast-glob": "^3.3.3", "globby": "^13.2.2", "graphiql": "^4.0.2", - "npm-run-all": "^4.1.5", "openapi-types": "^12.1.3", "postcss": "catalog:", "remark-frontmatter": "^5.0.0", "remark-parse": "^11.0.0", - "shiki": "^3.2.1", + "shiki": "catalog:", "shx": "^0.4.0", "simple-git": "^3.24.0", "slugify": "^1.6.6", diff --git a/apps/docs/public/favicon/android-icon-144x144.png b/apps/docs/public/favicon/android-icon-144x144.png deleted file mode 100644 index 144190d1976..00000000000 Binary files a/apps/docs/public/favicon/android-icon-144x144.png and /dev/null differ diff --git a/apps/docs/public/favicon/android-icon-192x192.png b/apps/docs/public/favicon/android-icon-192x192.png index c4f30c5f922..af255b04ff9 100644 Binary files a/apps/docs/public/favicon/android-icon-192x192.png and b/apps/docs/public/favicon/android-icon-192x192.png differ diff --git a/apps/docs/public/favicon/android-icon-36x36.png b/apps/docs/public/favicon/android-icon-36x36.png deleted file mode 100644 index aed5d2b3063..00000000000 Binary files a/apps/docs/public/favicon/android-icon-36x36.png and /dev/null differ diff --git a/apps/docs/public/favicon/android-icon-48x48.png b/apps/docs/public/favicon/android-icon-48x48.png deleted file mode 100644 index 0821ae73784..00000000000 Binary files a/apps/docs/public/favicon/android-icon-48x48.png and /dev/null differ diff --git a/apps/docs/public/favicon/android-icon-512x512.png b/apps/docs/public/favicon/android-icon-512x512.png new file mode 100644 index 00000000000..a95b768d8f7 Binary files /dev/null and b/apps/docs/public/favicon/android-icon-512x512.png differ diff --git a/apps/docs/public/favicon/android-icon-72x72.png b/apps/docs/public/favicon/android-icon-72x72.png deleted file mode 100644 index 10d2bbaaa86..00000000000 Binary files a/apps/docs/public/favicon/android-icon-72x72.png and /dev/null differ diff --git a/apps/docs/public/favicon/android-icon-96x96.png b/apps/docs/public/favicon/android-icon-96x96.png deleted file mode 100644 index a8d96ffed73..00000000000 Binary files a/apps/docs/public/favicon/android-icon-96x96.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-114x114.png b/apps/docs/public/favicon/apple-icon-114x114.png deleted file mode 100644 index 396cee8516a..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-114x114.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-120x120.png b/apps/docs/public/favicon/apple-icon-120x120.png deleted file mode 100644 index 7e4e727910e..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-120x120.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-144x144.png b/apps/docs/public/favicon/apple-icon-144x144.png deleted file mode 100644 index 144190d1976..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-144x144.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-152x152.png b/apps/docs/public/favicon/apple-icon-152x152.png deleted file mode 100644 index cb261b8bba0..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-152x152.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-180x180.png b/apps/docs/public/favicon/apple-icon-180x180.png index 3cf7f1132c8..41f699b14ff 100644 Binary files a/apps/docs/public/favicon/apple-icon-180x180.png and b/apps/docs/public/favicon/apple-icon-180x180.png differ diff --git a/apps/docs/public/favicon/apple-icon-57x57.png b/apps/docs/public/favicon/apple-icon-57x57.png deleted file mode 100644 index 278a7a87bee..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-57x57.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-60x60.png b/apps/docs/public/favicon/apple-icon-60x60.png deleted file mode 100644 index a693570eea3..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-60x60.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-72x72.png b/apps/docs/public/favicon/apple-icon-72x72.png deleted file mode 100644 index 10d2bbaaa86..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-72x72.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-76x76.png b/apps/docs/public/favicon/apple-icon-76x76.png deleted file mode 100644 index 241bd6a2452..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-76x76.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon-precomposed.png b/apps/docs/public/favicon/apple-icon-precomposed.png deleted file mode 100644 index c4f30c5f922..00000000000 Binary files a/apps/docs/public/favicon/apple-icon-precomposed.png and /dev/null differ diff --git a/apps/docs/public/favicon/apple-icon.png b/apps/docs/public/favicon/apple-icon.png deleted file mode 100644 index c4f30c5f922..00000000000 Binary files a/apps/docs/public/favicon/apple-icon.png and /dev/null differ diff --git a/apps/docs/public/favicon/browserconfig.xml b/apps/docs/public/favicon/browserconfig.xml deleted file mode 100644 index 1a8136def88..00000000000 --- a/apps/docs/public/favicon/browserconfig.xml +++ /dev/null @@ -1,2 +0,0 @@ - -#ffffff \ No newline at end of file diff --git a/apps/docs/public/favicon/favicon-128.png b/apps/docs/public/favicon/favicon-128.png deleted file mode 100644 index 4fab8e34978..00000000000 Binary files a/apps/docs/public/favicon/favicon-128.png and /dev/null differ diff --git a/apps/docs/public/favicon/favicon-16x16.png b/apps/docs/public/favicon/favicon-16x16.png deleted file mode 100644 index 7d05a72096b..00000000000 Binary files a/apps/docs/public/favicon/favicon-16x16.png and /dev/null differ diff --git a/apps/docs/public/favicon/favicon-180x180.png b/apps/docs/public/favicon/favicon-180x180.png deleted file mode 100644 index 3cf7f1132c8..00000000000 Binary files a/apps/docs/public/favicon/favicon-180x180.png and /dev/null differ diff --git a/apps/docs/public/favicon/favicon-196x196.png b/apps/docs/public/favicon/favicon-196x196.png deleted file mode 100644 index 1e0258526ae..00000000000 Binary files a/apps/docs/public/favicon/favicon-196x196.png and /dev/null differ diff --git a/apps/docs/public/favicon/favicon-32x32.png b/apps/docs/public/favicon/favicon-32x32.png deleted file mode 100644 index 7eb8cabea60..00000000000 Binary files a/apps/docs/public/favicon/favicon-32x32.png and /dev/null differ diff --git a/apps/docs/public/favicon/favicon-48x48.png b/apps/docs/public/favicon/favicon-48x48.png deleted file mode 100644 index 40377a51b54..00000000000 Binary files a/apps/docs/public/favicon/favicon-48x48.png and /dev/null differ diff --git a/apps/docs/public/favicon/favicon-96x96.png b/apps/docs/public/favicon/favicon-96x96.png deleted file mode 100644 index a8d96ffed73..00000000000 Binary files a/apps/docs/public/favicon/favicon-96x96.png and /dev/null differ diff --git a/apps/docs/public/favicon/favicon.ico b/apps/docs/public/favicon/favicon.ico index 343ad561d71..7a0c86a9c0f 100644 Binary files a/apps/docs/public/favicon/favicon.ico and b/apps/docs/public/favicon/favicon.ico differ diff --git a/apps/docs/public/favicon/local/android-icon-192x192.png b/apps/docs/public/favicon/local/android-icon-192x192.png new file mode 100644 index 00000000000..a174cf0468a Binary files /dev/null and b/apps/docs/public/favicon/local/android-icon-192x192.png differ diff --git a/apps/docs/public/favicon/local/android-icon-512x512.png b/apps/docs/public/favicon/local/android-icon-512x512.png new file mode 100644 index 00000000000..a5fc7d6b949 Binary files /dev/null and b/apps/docs/public/favicon/local/android-icon-512x512.png differ diff --git a/apps/docs/public/favicon/local/apple-icon-180x180.png b/apps/docs/public/favicon/local/apple-icon-180x180.png new file mode 100644 index 00000000000..775ce59ec9b Binary files /dev/null and b/apps/docs/public/favicon/local/apple-icon-180x180.png differ diff --git a/apps/docs/public/favicon/local/favicon.ico b/apps/docs/public/favicon/local/favicon.ico new file mode 100644 index 00000000000..7cc4cd192a9 Binary files /dev/null and b/apps/docs/public/favicon/local/favicon.ico differ diff --git a/apps/docs/public/favicon/local/manifest.json b/apps/docs/public/favicon/local/manifest.json new file mode 100644 index 00000000000..8b55cf1ecb1 --- /dev/null +++ b/apps/docs/public/favicon/local/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Supabase", + "short_name": "Supabase", + "description": "The Postgres Development Platform.", + "display": "standalone", + "theme_color": "#1C1C1C", + "background_color": "#1C1C1C", + "icons": [ + { + "src": "android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/apps/docs/public/favicon/manifest.json b/apps/docs/public/favicon/manifest.json index b3de4e47bae..8b55cf1ecb1 100644 --- a/apps/docs/public/favicon/manifest.json +++ b/apps/docs/public/favicon/manifest.json @@ -7,40 +7,16 @@ "background_color": "#1C1C1C", "icons": [ { - "src": "/favicon/android-icon-36x36.png", - "sizes": "36x36", - "type": "image/png", - "density": "0.75" - }, - { - "src": "/favicon/android-icon-48x48.png", - "sizes": "48x48", - "type": "image/png", - "density": "1.0" - }, - { - "src": "/favicon/android-icon-72x72.png", - "sizes": "72x72", - "type": "image/png", - "density": "1.5" - }, - { - "src": "/favicon/android-icon-96x96.png", - "sizes": "96x96", - "type": "image/png", - "density": "2.0" - }, - { - "src": "/favicon/android-icon-144x144.png", - "sizes": "144x144", - "type": "image/png", - "density": "3.0" - }, - { - "src": "/favicon/android-icon-192x192.png", + "src": "android-icon-192x192.png", "sizes": "192x192", "type": "image/png", - "density": "4.0" + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" } ] } diff --git a/apps/docs/public/favicon/ms-icon-144x144.png b/apps/docs/public/favicon/ms-icon-144x144.png deleted file mode 100644 index 144190d1976..00000000000 Binary files a/apps/docs/public/favicon/ms-icon-144x144.png and /dev/null differ diff --git a/apps/docs/public/favicon/ms-icon-150x150.png b/apps/docs/public/favicon/ms-icon-150x150.png deleted file mode 100644 index bbdf205047c..00000000000 Binary files a/apps/docs/public/favicon/ms-icon-150x150.png and /dev/null differ diff --git a/apps/docs/public/favicon/ms-icon-310x310.png b/apps/docs/public/favicon/ms-icon-310x310.png deleted file mode 100644 index 42bd0698a0f..00000000000 Binary files a/apps/docs/public/favicon/ms-icon-310x310.png and /dev/null differ diff --git a/apps/docs/public/favicon/ms-icon-70x70.png b/apps/docs/public/favicon/ms-icon-70x70.png deleted file mode 100644 index 6a428df84d3..00000000000 Binary files a/apps/docs/public/favicon/ms-icon-70x70.png and /dev/null differ diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index d2818d40a2b..0198dcece3c 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -44,6 +44,7 @@ Beng Eu Benjamin Coenen Beth Long Bobbie Soedirgo +Brad Deam Brad Gyger Brendan Stephens Brent Graveland @@ -79,6 +80,7 @@ Connor Davis Craig Cannon Cuong Do Dane Harrigan +Daniel Chapman Daniel Guerra Daniel Mitterdorfer Daniel Velázquez Lara @@ -102,6 +104,7 @@ Divit D Divya Sharma Donna Alexandra Douglas Hunley +Drew McLellan Eduardo Gurgel Eleftheria Trivyzaki Eliot Whalan @@ -124,6 +127,7 @@ Garrett Crowell Gerardo Estaba Gildas Garcia Giuseppe Mandato +Gonzalo Servat Greg Kress Greg Marzouka Greg P @@ -171,7 +175,6 @@ John Jarvis Joel Martin Johan Bergström John Pena -John Schaeffer Jon M Jonathan Smock Jonny Summers-Muir @@ -228,6 +231,7 @@ Márton Boros Matthew Hambright Matt Hudson Matt Johnston +Matt McGill Matt Robinson Matt Rossman Matt Smiley @@ -237,6 +241,7 @@ Mert Yerekapan Michal Kleczek Michal Olszewski Michelle Jubrey +Mike Donnalley Mike Podobnik Miranda Limonczenko Mo Khan @@ -257,8 +262,6 @@ Pamela Chia Patricia Szasz Patrick Crane Paul Bauer -Paul Caselton -Paul Cioanca Paul Copplestone Paul Ibeabuchi Pavel Borisov diff --git a/apps/docs/scripts/search/sources/github-discussion.ts b/apps/docs/scripts/search/sources/github-discussion.ts deleted file mode 100644 index fc29ce7e7c8..00000000000 --- a/apps/docs/scripts/search/sources/github-discussion.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { createAppAuth } from '@octokit/auth-app' -import { Octokit } from '@octokit/core' -import { paginateGraphql } from '@octokit/plugin-paginate-graphql' -import crypto, { createHash } from 'node:crypto' -import { BaseLoader, BaseSource } from './base.js' - -export const ExtendedOctokit = Octokit.plugin(paginateGraphql) -export type ExtendedOctokit = InstanceType - -export type Discussion = { - id: string - updatedAt: string - url: string - title: string - body: string - databaseId: number -} - -export type DiscussionsResponse = { - repository: { - discussions: { - totalCount: number - nodes: Discussion[] - } - } -} - -const appId = process.env.DOCS_GITHUB_APP_ID -const installationId = process.env.DOCS_GITHUB_APP_INSTALLATION_ID -const privateKey = process.env.DOCS_GITHUB_APP_PRIVATE_KEY - -/** - * Fetches GitHub discussions for a repository + category - */ -export async function fetchDiscussions(owner: string, repo: string, categoryId: string) { - const octokit = new ExtendedOctokit({ - authStrategy: createAppAuth, - auth: { - appId, - installationId, - privateKey: crypto.createPrivateKey(privateKey!).export({ type: 'pkcs8', format: 'pem' }), - }, - }) - - const { - repository: { - discussions: { nodes: discussions }, - }, - } = await octokit.graphql.paginate( - ` - query troubleshootDiscussions($cursor: String, $owner: String!, $repo: String!, $categoryId: ID!) { - repository(owner: $owner, name: $repo) { - discussions(first: 100, after: $cursor, categoryId: $categoryId) { - totalCount - nodes { - id - updatedAt - url - title - body - databaseId - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - `, - { - owner, - repo, - categoryId, - } - ) - - return discussions -} - -export class GitHubDiscussionLoader extends BaseLoader { - type = 'github-discussions' as const - - constructor( - source: string, - public discussion: Discussion - ) { - super(source, discussion.url) - } - - async load() { - return [new GitHubDiscussionSource(this.source, this.path, this.discussion)] - } -} - -export class GitHubDiscussionSource extends BaseSource { - type = 'github-discussions' as const - - constructor( - source: string, - path: string, - public discussion: Discussion - ) { - super(source, path) - } - - async process() { - const { id, title, updatedAt, body, databaseId } = this.discussion - - const checksum = createHash('sha256').update(updatedAt).digest('base64') - - const meta = { id, title, updatedAt } - - // Currently the discussion post itself is being considered as the answer - // (as opposed to a comment marked as answer) - // So we link the slug to the initial discussion post rather than a comment answer - const slug = `discussion-${databaseId}` - - // Format the discussion title + body as markdown for better embeddings + LLM response - const content = `# ${title}\n${body}` - - // For now, only a single section is created for GH discussions - // Consider adding multiple if we want to include comments/answers - const sections = [ - { - heading: title, - slug, - content, - }, - ] - - this.checksum = checksum - this.meta = meta - this.sections = sections - - return { - checksum, - meta, - sections, - } - } - - extractIndexedContent(): string { - const sections = this.sections ?? [] - return sections.map(({ heading, content }) => `${heading}\n\n${content}`).join('\n') - } -} diff --git a/apps/docs/scripts/troubleshooting/watch.mjs b/apps/docs/scripts/troubleshooting/watch.mjs deleted file mode 100644 index 304954500bf..00000000000 --- a/apps/docs/scripts/troubleshooting/watch.mjs +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-check - -import chokidar from 'chokidar' -import { closeSync, openSync, readFileSync, writeSync } from 'node:fs' -import { basename, join } from 'node:path' - -const TROUBLESHOOTING_DIRECTORY = join(import.meta.dirname, '../../content/troubleshooting') - -let __template = '' - -function getTemplate() { - if (!__template) { - __template = readFileSync(join(TROUBLESHOOTING_DIRECTORY, '_template.mdx'), 'utf8') - } - return __template -} - -const watcher = chokidar - .watch(TROUBLESHOOTING_DIRECTORY, { ignoreInitial: true }) - .on('add', (path, stats) => { - if (!stats?.isFile() || basename(path).startsWith('_')) return - - console.log(`File ${path} has been added.`) - - if (stats?.size !== 0) { - console.log(`File ${path} already has content. Skipping template writing.`) - return - } - - try { - console.log(`Writing template to ${path}...`) - const fd = openSync(path, 'w') - writeSync(fd, getTemplate()) - closeSync(fd) - } catch (err) { - console.error(`Error writing template to ${path}:`, err) - } - }) - -// Keep the process alive -const interval = setInterval(() => {}, 1000) - -const shutdown = () => { - console.log(`\nShutting down watcher...`) - clearInterval(interval) - watcher - .close() - .then(() => { - console.log('Watcher closed. Exiting process.') - process.exit(0) - }) - .catch((err) => { - console.error('Error closing watcher:', err) - process.exit(1) - }) -} - -process.on('SIGINT', shutdown) -process.on('SIGTERM', shutdown) -process.on('SIGHUP', shutdown) -process.on('SIGQUIT', shutdown) diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index 0af1323dc4a..dcc6a0a8a3d 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -2947,7 +2947,7 @@ commands: Reset local database to match migrations first? (local data will be lost) [y/N] y Resetting database... ... - Declarative schema written to supabase/database + Declarative schema written to supabase/schemas Finished supabase db schema declarative generate. tags: [] links: [] @@ -2984,7 +2984,6 @@ commands: - id: experimental name: --experimental description: enable experimental features - required: true default_value: 'false' - id: no-cache name: --no-cache @@ -3048,7 +3047,6 @@ commands: - id: experimental name: --experimental description: enable experimental features - required: true default_value: 'false' - id: no-cache name: --no-cache @@ -3240,7 +3238,12 @@ commands: If no entries exist in the migration history table, the behavior depends on the diff engine. Under `pg-delta`, the remote database is diffed against an empty shadow database. Under the legacy `migra` engine, the CLI uses `pg_dump` to capture all contents of the remote schemas you have created. Otherwise, this command only diffs schema changes against the remote database, similar to running `db diff --linked`. - When `[experimental.pgdelta] enabled = true`, the migration-file `db pull` workflow uses pg-delta for the shadow diff step; pass `--diff-engine migra` to fall back for a single run. Projects without that setting use migra; pass `--diff-engine pg-delta` to use pg-delta for a single run. Pass `--declarative` to replace the declarative schema tree (`supabase/schemas` by default, configurable with `experimental.pgdelta.declarative_schema_path`) from the selected database instead of creating a migration. + When `[experimental.pgdelta] enabled = true`, the migration-file `db pull` workflow uses pg-delta for the shadow diff step; pass `--diff-engine migra` to fall back for a single run. Projects without that setting use migra; pass `--diff-engine pg-delta` to use pg-delta for a single run. Pass `--declarative` to export the selected database into the declarative schema tree (`supabase/schemas` by default, configurable with `experimental.pgdelta.declarative_schema_path`) instead of creating a migration. + + On a project without pg-delta enabled, don't use `--declarative`. The export also points `[db.migrations].schema_paths` at the tree it writes. The next local `db diff` applies those files in lexicographic order without resolving dependencies between them, so it fails when a file sorts before an object it depends on. If you've already run it, enable pg-delta, or undo both changes: + + 1. Delete the files the export wrote to the declarative directory (`supabase/schemas/` by default). Files that were already there are left in place. + 2. Restore `[db.migrations].schema_paths` in `config.toml` to its previous value, or remove it if the export added it. Under the pg-delta engine, your own customizations in Supabase-managed schemas are captured automatically, such as triggers on managed tables like `auth.users` and RLS policies on `storage.objects` and `storage.buckets`. Under the legacy migra engine, the initial pull's `pg_dump` skips managed schemas, but the diff pass that follows appends your trigger and RLS policy changes there to the same migration. If an older CLI reports that `auth` and `storage` were excluded, capture existing customizations there with `--schema auth,storage`. examples: @@ -3264,7 +3267,6 @@ commands: Diffing schemas... No schema changes found The remote database is already in sync with your local migrations — nothing to pull. - exit status 1 - id: custom-schemas name: Custom schemas code: supabase db pull --schema public,extensions @@ -3276,7 +3278,6 @@ commands: Diffing schemas: public,extensions No schema changes found The remote database is already in sync with your local migrations — nothing to pull. - exit status 1 tags: [] links: [] usage: supabase db pull [migration name] [flags] @@ -3524,7 +3525,7 @@ commands: Requires the local development stack to be running when diffing against the local database. To diff against a remote or self-hosted database, specify the `--linked` or `--db-url` flag respectively. - Compares schema differences between the target database and a shadow database. Projects with `[experimental.pgdelta] enabled = true` use the bundled pg-delta engine; other projects run the legacy [djrobstep/migra](https://github.com/djrobstep/migra) engine in a container. The shadow database is created by applying migrations in local `supabase/migrations` directory in a separate container. Output is written to stdout by default. For convenience, you can also save the schema diff as a new migration file by passing in `-f` flag. + Compares schema differences between the target database and a shadow database. Projects with `[experimental.pgdelta] enabled = true` use the bundled pg-delta engine; other projects run the legacy [djrobstep/migra](https://github.com/djrobstep/migra) engine in a container, unless you pass `--use-pg-delta` for a single run. The shadow database is created by applying migrations in local `supabase/migrations` directory in a separate container. Output is written to stdout by default. For convenience, you can also save the schema diff as a new migration file by passing in `-f` flag. By default, all schemas in the target database are diffed. Use the `--schema public,extensions` flag to restrict diffing to a subset of schemas. diff --git a/apps/docs/spec/cli_v1_config.yaml b/apps/docs/spec/cli_v1_config.yaml index fb2436569d0..b956dbb6ac5 100644 --- a/apps/docs/spec/cli_v1_config.yaml +++ b/apps/docs/spec/cli_v1_config.yaml @@ -468,7 +468,7 @@ parameters: default: '[]' description: | An ordered list of schema files, directories, or glob patterns describing your database, relative to the `supabase` directory. For example: `["./schemas/*.sql"]`. - Only the legacy `migra` diff engine uses this setting, where it controls the order in which declarative schema files are applied. The `pg-delta` diff engine (see `experimental.pgdelta.enabled`) ignores it and orders declarative files automatically based on the dependencies between statements. + Only the legacy `migra` diff engine uses this setting, where it controls the order in which declarative schema files are applied. On that engine, `supabase db pull --declarative` sets it to the declarative schema directory automatically. The `pg-delta` diff engine (see `experimental.pgdelta.enabled`) ignores it and orders declarative files by the dependencies between statements. It warns when the setting lists any paths. links: - name: 'Declarative database schemas' link: 'https://supabase.com/docs/guides/local-development/declarative-database-schemas' @@ -1855,6 +1855,7 @@ parameters: default: './schemas' description: | The directory where the `pg-delta` engine reads and writes declarative schema files. Relative paths resolve from the `supabase` directory. + CLI versions before 2.115.0 defaulted to `./database`. If that directory still holds schema files, `./schemas` is empty, and this setting is unset, the CLI warns and reads `./schemas` instead. To keep that tree, set `./database` here. Note: This is an experimental feature and may change in future releases. links: - name: 'Declarative database schemas' diff --git a/apps/docs/spec/reference/server/v1/partials/frameworks.mdx b/apps/docs/spec/reference/server/v1/partials/frameworks.mdx index 3e29ea0d870..49fdcf7dee9 100644 --- a/apps/docs/spec/reference/server/v1/partials/frameworks.mdx +++ b/apps/docs/spec/reference/server/v1/partials/frameworks.mdx @@ -5,7 +5,7 @@ title: Frameworks `@supabase/server` middleware runs inside Hono, H3, Elysia, NestJS, and TanStack Start through a bridge: one file you copy into your project. The bridge runs a `@supabase/middleware` entry array in the framework's own middleware slot. Your route handlers then read `supabase`, `jwtClaims`, and any other key the entries contribute from the place your framework keeps per-request state. -The framework adapters (`@supabase/server/adapters/*`) predate the middleware engine. They are deprecated and will be removed on December 1, 2026. The bridges on this page replace them. If you use an adapter today, read "Moving off the adapters" below before you change anything. One step in that migration can open an endpoint up without an error. +The framework adapters (`@supabase/server/adapters/*`) predate the middleware engine. They are deprecated and will be removed on December 1, 2026. The bridges on this page replace them. If you use an adapter today, read [Moving off the adapters](/docs/reference/server/frameworks#moving-off-the-adapters) before you change anything. One step in that migration can open an endpoint up without an error. diff --git a/apps/kb/.env.example b/apps/kb/.env.example new file mode 100644 index 00000000000..eed0248c52f --- /dev/null +++ b/apps/kb/.env.example @@ -0,0 +1,20 @@ +# GitHub auth for scripts/federated-content/fetch-federated-content.ts (pulls +# troubleshooting content from the private supabase/troubleshooting repo). +# Same credentials as apps/docs — that GitHub App must also be granted +# access to supabase/troubleshooting. +# +# Preferred: a GitHub App installation. Set all three or none. +KB_GITHUB_APP_ID= +KB_GITHUB_APP_INSTALLATION_ID= +KB_GITHUB_APP_PRIVATE_KEY= + +# Fallback for local dev if the App isn't configured: a personal access +# token, e.g. `export GH_TOKEN=$(gh auth token)`. +GH_TOKEN= + +# Supabase project backing troubleshooting_entries (same project apps/www and +# apps/studio use — supabase/config.toml's `[remotes.prod]`). Everything in kb +# uses the service role key: the Astro build reads with it, the sync scripts +# also write. Never expose it to client code (kb is static, build-time only). +SUPABASE_URL= +SUPABASE_SERVICE_ROLE_KEY= diff --git a/apps/kb/.gitignore b/apps/kb/.gitignore index 557757712a2..57a0cf1c109 100644 --- a/apps/kb/.gitignore +++ b/apps/kb/.gitignore @@ -4,6 +4,9 @@ dist/ # generated markdown export (scripts/generate-markdown.mjs) public/markdown/ +# federated troubleshooting content (scripts/federated-content), regenerated on every prebuild +src/content/troubleshooting/ + # generated types .astro/ @@ -25,3 +28,6 @@ pnpm-debug.log* # jetbrains setting folder .idea/ + +# jetbrains setting folder +src/content/troubleshooting diff --git a/apps/kb/AGENTS.md b/apps/kb/AGENTS.md index de2bb5975a3..e333f62d860 100644 --- a/apps/kb/AGENTS.md +++ b/apps/kb/AGENTS.md @@ -50,13 +50,14 @@ Content lives under `src/content/guides/*.{md,mdx}`. Keep it plain GitHub-Flavor ## Pages and markdown export Each content collection renders through a matching catch-all page — e.g. `src/content/guides/**` → -`src/pages/guides/[...slug].astro` → `GuideLayout`. Topic pages (`src/pages/topics/[topic].astro`) are -generated from the `TOPICS` list in `src/lib/topics.ts`, not from content files. +`src/pages/guides/[...slug].astro` → `ArticleLayout` (shared with `troubleshooting`, see below). Topic pages +(`src/pages/topics/[topic].astro`) are generated from the `TOPICS` list in `src/lib/topics.ts`, not from +content files, and read from every collection. Separately, `scripts/generate-markdown.mjs` runs as a `prebuild` step and exports every content file — plus one page per topic, listing its guides — as a plain `.md` file under the gitignored `public/markdown/`, mirroring the page's URL with a `.md` extension. `vercel.json` permanently redirects `.md` requests to -these generated files, one redirect entry per route section (`guides`, `topics`). +these generated files, one redirect entry per route section (`guides`, `topics`, `troubleshooting`). If you add a new content collection or top-level route, add a matching redirect in `vercel.json` (`/kb/
/:path+.md` → `/kb/markdown/
/:path+.md`), and check whether `generate-markdown.mjs` @@ -66,3 +67,30 @@ per-topic-style listing pages don't. ## Topic-specific guidance Articles tagged with the `Comparison` topic are primarily oriented towards LLM crawlers (and not human readers). Because of this, these articles are hidden from the main site navigation. + +## Federated troubleshooting content + +`src/content/troubleshooting/` is never hand-authored — it's gitignored and regenerated every `prebuild` by +`scripts/federated-content/fetch-federated-content.ts`, which fetches it from the private +`supabase/troubleshooting` repo. It has its own collection and route (`src/pages/troubleshooting/[...slug].astro`), +separate from `guides`. + +A few things here are intentional, not bugs to fix: + +- Most source files use TOML frontmatter, which the script can't parse — those are filtered out (not written + to disk) and summarized in a build warning, rather than guessed at. +- No link rewriting or markdown reprocessing — body content is written exactly as fetched. +- Output is `.md`, not `.mdx` — this source isn't MDX-safe (e.g. GFM autolinks break MDX's JSX parser). +- `topics` are passed through verbatim from the source, unvalidated against `TOPIC_NAMES` — unlike `guides`. + `description` is optional too, since the source doesn't provide one. + +Auth reuses the docs GitHub App env vars (`KB_GITHUB_APP_ID`/`_INSTALLATION_ID`/`_PRIVATE_KEY`), falling back +to `GH_TOKEN`/`GITHUB_TOKEN` for local dev — see `.env.example`. + +Syncing guides to GitHub Discussions (`scripts/federated-content/sync-troubleshooting-entries.ts` and +`sync-troubleshooting-updates.ts`) is a separate concern from fetching — see `.github/workflows/kb-troubleshooting-sync.yml` +and the README's "Workflows" section. Those scripts are CI-only, never part of `prebuild`. + +`backfill-legacy-slugs.ts` is a one-time migration aid for the 278 pre-existing (apps/docs-origin) rows in +`troubleshooting_entries` — safe to delete (with its JSON file, package.json script, and workflow step) once +those rows are all backfilled; see the comment at the top of that file. diff --git a/apps/kb/README.md b/apps/kb/README.md index 414a13a6619..1927fb48bc0 100644 --- a/apps/kb/README.md +++ b/apps/kb/README.md @@ -44,3 +44,17 @@ All commands are run from the root of the project, from a terminal: ## 👀 Want to learn more? Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). + +## Federated troubleshooting content + +`src/content/troubleshooting/` isn't hand-written — it's fetched from a private repo by +`scripts/federated-content/fetch-federated-content.ts` as part of the build (`pnpm run fetch:federated-content`) +and is gitignored. See `AGENTS.md` for how it works and what's intentionally left out for now. + +## Workflows + +`.github/workflows/kb-troubleshooting-sync.yml` runs on every push to `master`: it fetches troubleshooting +content, creates a GitHub Discussion for any guide that doesn't have one yet +(`sync:troubleshooting-entries`), and pushes title/content updates to discussions whose guide has since +changed (`sync:troubleshooting-updates`). This is separate from kb's own build — it has real side effects +(creating/updating Discussions, writing DB rows), so it never runs as part of `pnpm build`/`prebuild`. diff --git a/apps/kb/astro.config.mjs b/apps/kb/astro.config.mjs index c8f0b142188..8715ce6f940 100644 --- a/apps/kb/astro.config.mjs +++ b/apps/kb/astro.config.mjs @@ -8,6 +8,7 @@ import react from '@astrojs/react' import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'astro/config' +import { BASE_PATH } from './src/lib/constants.ts' import rehypeAdmonitions from './src/lib/mdx/rehype-admonitions.js' import supabaseTheme from '../learn/lib/themes/supabase-2.json' with { type: 'json' } @@ -44,7 +45,7 @@ const ssrLodashEs = { // https://astro.build/config export default defineConfig({ site: 'https://supabase.com', - base: '/kb', + base: BASE_PATH, trailingSlash: 'ignore', integrations: [react(), // rehype-admonitions is a custom rehype plugin and Astro's default pipeline is // satteri, so .mdx gets its own unified processor. Plain .md stays on satteri. @@ -58,6 +59,7 @@ export default defineConfig({ noExternal: ['lodash'], }, plugins: [tailwindcss(), ssrLodashEs], + logLevel: 'error', }, markdown: { shikiConfig: { diff --git a/apps/kb/package.json b/apps/kb/package.json index da41712a222..029eaf745dd 100644 --- a/apps/kb/package.json +++ b/apps/kb/package.json @@ -7,9 +7,13 @@ }, "scripts": { "dev": "astro dev --port 3008", - "prebuild": "pnpm run generate:markdown", + "prebuild": "pnpm run fetch:federated-content && pnpm run generate:markdown", "build": "astro build", - "generate:markdown": "node --experimental-strip-types ./scripts/generate-markdown.mjs", + "fetch:federated-content": "tsx ./scripts/federated-content/fetch-federated-content.ts", + "generate:markdown": "tsx ./scripts/generate-markdown.mjs", + "sync:legacy-slugs": "tsx ./scripts/federated-content/backfill-legacy-slugs.ts", + "sync:troubleshooting-entries": "tsx ./scripts/federated-content/sync-troubleshooting-entries.ts", + "sync:troubleshooting-updates": "tsx ./scripts/federated-content/sync-troubleshooting-updates.ts", "preview": "astro preview", "astro": "astro", "test": "vitest --run" @@ -19,7 +23,13 @@ "@astrojs/mdx": "^7.0.7", "@astrojs/react": "^6.0.4", "@astrojs/sitemap": "^3.7.4", + "@octokit/auth-app": "^7.0.0", + "@octokit/core": "^7.0.6", + "@octokit/plugin-retry": "^8.1.0", + "@supabase/supabase-js": "catalog:", "astro": "^7.2.6", + "common": "workspace:*", + "gray-matter": "^4.0.3", "lucide-react": "*", "react": "catalog:", "react-dom": "catalog:", @@ -39,8 +49,10 @@ "@types/react": "catalog:", "@types/react-dom": "catalog:", "config": "workspace:*", + "dotenv": "^16.5.0", "lodash-es": "catalog:", "tailwindcss": "catalog:", + "tsx": "catalog:", "vitest": "catalog:" }, "allowScripts": { diff --git a/apps/kb/public/favicon/android-icon-192x192.png b/apps/kb/public/favicon/android-icon-192x192.png new file mode 100644 index 00000000000..af255b04ff9 Binary files /dev/null and b/apps/kb/public/favicon/android-icon-192x192.png differ diff --git a/apps/kb/public/favicon/android-icon-512x512.png b/apps/kb/public/favicon/android-icon-512x512.png new file mode 100644 index 00000000000..a95b768d8f7 Binary files /dev/null and b/apps/kb/public/favicon/android-icon-512x512.png differ diff --git a/apps/kb/public/favicon/apple-icon-114x114.png b/apps/kb/public/favicon/apple-icon-114x114.png deleted file mode 100644 index 396cee8516a..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-114x114.png and /dev/null differ diff --git a/apps/kb/public/favicon/apple-icon-120x120.png b/apps/kb/public/favicon/apple-icon-120x120.png deleted file mode 100644 index 7e4e727910e..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-120x120.png and /dev/null differ diff --git a/apps/kb/public/favicon/apple-icon-144x144.png b/apps/kb/public/favicon/apple-icon-144x144.png deleted file mode 100644 index 144190d1976..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-144x144.png and /dev/null differ diff --git a/apps/kb/public/favicon/apple-icon-152x152.png b/apps/kb/public/favicon/apple-icon-152x152.png deleted file mode 100644 index cb261b8bba0..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-152x152.png and /dev/null differ diff --git a/apps/kb/public/favicon/apple-icon-180x180.png b/apps/kb/public/favicon/apple-icon-180x180.png new file mode 100644 index 00000000000..41f699b14ff Binary files /dev/null and b/apps/kb/public/favicon/apple-icon-180x180.png differ diff --git a/apps/kb/public/favicon/apple-icon-57x57.png b/apps/kb/public/favicon/apple-icon-57x57.png deleted file mode 100644 index 278a7a87bee..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-57x57.png and /dev/null differ diff --git a/apps/kb/public/favicon/apple-icon-60x60.png b/apps/kb/public/favicon/apple-icon-60x60.png deleted file mode 100644 index a693570eea3..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-60x60.png and /dev/null differ diff --git a/apps/kb/public/favicon/apple-icon-72x72.png b/apps/kb/public/favicon/apple-icon-72x72.png deleted file mode 100644 index 10d2bbaaa86..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-72x72.png and /dev/null differ diff --git a/apps/kb/public/favicon/apple-icon-76x76.png b/apps/kb/public/favicon/apple-icon-76x76.png deleted file mode 100644 index 241bd6a2452..00000000000 Binary files a/apps/kb/public/favicon/apple-icon-76x76.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon-128.png b/apps/kb/public/favicon/favicon-128.png deleted file mode 100644 index 4fab8e34978..00000000000 Binary files a/apps/kb/public/favicon/favicon-128.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon-16x16.png b/apps/kb/public/favicon/favicon-16x16.png deleted file mode 100644 index 7d05a72096b..00000000000 Binary files a/apps/kb/public/favicon/favicon-16x16.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon-180x180.png b/apps/kb/public/favicon/favicon-180x180.png deleted file mode 100644 index 3cf7f1132c8..00000000000 Binary files a/apps/kb/public/favicon/favicon-180x180.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon-196x196.png b/apps/kb/public/favicon/favicon-196x196.png deleted file mode 100644 index 1e0258526ae..00000000000 Binary files a/apps/kb/public/favicon/favicon-196x196.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon-32x32.png b/apps/kb/public/favicon/favicon-32x32.png deleted file mode 100644 index 7eb8cabea60..00000000000 Binary files a/apps/kb/public/favicon/favicon-32x32.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon-48x48.png b/apps/kb/public/favicon/favicon-48x48.png deleted file mode 100644 index 40377a51b54..00000000000 Binary files a/apps/kb/public/favicon/favicon-48x48.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon-96x96.png b/apps/kb/public/favicon/favicon-96x96.png deleted file mode 100644 index a8d96ffed73..00000000000 Binary files a/apps/kb/public/favicon/favicon-96x96.png and /dev/null differ diff --git a/apps/kb/public/favicon/favicon.ico b/apps/kb/public/favicon/favicon.ico index 343ad561d71..7a0c86a9c0f 100644 Binary files a/apps/kb/public/favicon/favicon.ico and b/apps/kb/public/favicon/favicon.ico differ diff --git a/apps/kb/public/favicon/local/android-icon-192x192.png b/apps/kb/public/favicon/local/android-icon-192x192.png new file mode 100644 index 00000000000..a174cf0468a Binary files /dev/null and b/apps/kb/public/favicon/local/android-icon-192x192.png differ diff --git a/apps/kb/public/favicon/local/android-icon-512x512.png b/apps/kb/public/favicon/local/android-icon-512x512.png new file mode 100644 index 00000000000..a5fc7d6b949 Binary files /dev/null and b/apps/kb/public/favicon/local/android-icon-512x512.png differ diff --git a/apps/kb/public/favicon/local/apple-icon-180x180.png b/apps/kb/public/favicon/local/apple-icon-180x180.png new file mode 100644 index 00000000000..775ce59ec9b Binary files /dev/null and b/apps/kb/public/favicon/local/apple-icon-180x180.png differ diff --git a/apps/kb/public/favicon/local/favicon.ico b/apps/kb/public/favicon/local/favicon.ico new file mode 100644 index 00000000000..7cc4cd192a9 Binary files /dev/null and b/apps/kb/public/favicon/local/favicon.ico differ diff --git a/apps/kb/public/favicon/local/manifest.json b/apps/kb/public/favicon/local/manifest.json new file mode 100644 index 00000000000..815234ebf8f --- /dev/null +++ b/apps/kb/public/favicon/local/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "Supabase Knowledge Base", + "short_name": "Supabase", + "icons": [ + { + "src": "android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/apps/kb/public/favicon/manifest.json b/apps/kb/public/favicon/manifest.json new file mode 100644 index 00000000000..815234ebf8f --- /dev/null +++ b/apps/kb/public/favicon/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "Supabase Knowledge Base", + "short_name": "Supabase", + "icons": [ + { + "src": "android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/apps/kb/public/img/troubleshooting/0e7f6842-78fd-44f9-b463-425507815fb6.png b/apps/kb/public/img/troubleshooting/0e7f6842-78fd-44f9-b463-425507815fb6.png new file mode 100644 index 00000000000..b75cf3c3b3a Binary files /dev/null and b/apps/kb/public/img/troubleshooting/0e7f6842-78fd-44f9-b463-425507815fb6.png differ diff --git a/apps/kb/public/img/troubleshooting/10950be3-264b-4a41-bc71-3d431c2756b2.png b/apps/kb/public/img/troubleshooting/10950be3-264b-4a41-bc71-3d431c2756b2.png new file mode 100644 index 00000000000..6b62f61329f Binary files /dev/null and b/apps/kb/public/img/troubleshooting/10950be3-264b-4a41-bc71-3d431c2756b2.png differ diff --git a/apps/kb/public/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png b/apps/kb/public/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png new file mode 100644 index 00000000000..348f6b063a1 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/152d65ad-f0ed-47cf-8dcb-1e31c6221e71.png differ diff --git a/apps/kb/public/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png b/apps/kb/public/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png new file mode 100644 index 00000000000..274d1fd2604 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/18ed2c88-332e-4e66-b9b4-c37e99a39104.png differ diff --git a/apps/kb/public/img/troubleshooting/224379580-fa77bd31-bb58-47e6-90ce-64e140f32579.png b/apps/kb/public/img/troubleshooting/224379580-fa77bd31-bb58-47e6-90ce-64e140f32579.png new file mode 100644 index 00000000000..f236805dbbc Binary files /dev/null and b/apps/kb/public/img/troubleshooting/224379580-fa77bd31-bb58-47e6-90ce-64e140f32579.png differ diff --git a/apps/kb/public/img/troubleshooting/3eeb2435-dd1c-41bc-9557-44cabff38f59.png b/apps/kb/public/img/troubleshooting/3eeb2435-dd1c-41bc-9557-44cabff38f59.png new file mode 100644 index 00000000000..6e99baedca4 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/3eeb2435-dd1c-41bc-9557-44cabff38f59.png differ diff --git a/apps/kb/public/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png b/apps/kb/public/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png new file mode 100644 index 00000000000..34be98771e2 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/401_edge_functions_toggle_off_JWT_check.png differ diff --git a/apps/kb/public/img/troubleshooting/401_in_the_logs.png b/apps/kb/public/img/troubleshooting/401_in_the_logs.png new file mode 100644 index 00000000000..77aaac43325 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/401_in_the_logs.png differ diff --git a/apps/kb/public/img/troubleshooting/46f59019-6ff1-4bfb-9acd-d5b7bd282f89.png b/apps/kb/public/img/troubleshooting/46f59019-6ff1-4bfb-9acd-d5b7bd282f89.png new file mode 100644 index 00000000000..66c1a0b5a53 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/46f59019-6ff1-4bfb-9acd-d5b7bd282f89.png differ diff --git a/apps/kb/public/img/troubleshooting/47685206-7914-440e-a010-da62f5c38186.png b/apps/kb/public/img/troubleshooting/47685206-7914-440e-a010-da62f5c38186.png new file mode 100644 index 00000000000..ffa3f78d0bf Binary files /dev/null and b/apps/kb/public/img/troubleshooting/47685206-7914-440e-a010-da62f5c38186.png differ diff --git a/apps/kb/public/img/troubleshooting/47998bed-0b77-433a-bfed-63222beb2aee.png b/apps/kb/public/img/troubleshooting/47998bed-0b77-433a-bfed-63222beb2aee.png new file mode 100644 index 00000000000..2eb18732d1d Binary files /dev/null and b/apps/kb/public/img/troubleshooting/47998bed-0b77-433a-bfed-63222beb2aee.png differ diff --git a/apps/kb/public/img/troubleshooting/49e11154-296b-42b9-8036-8a7288e49b8a.png b/apps/kb/public/img/troubleshooting/49e11154-296b-42b9-8036-8a7288e49b8a.png new file mode 100644 index 00000000000..d56aedf13e5 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/49e11154-296b-42b9-8036-8a7288e49b8a.png differ diff --git a/apps/kb/public/img/troubleshooting/49e175d9-6338-4b17-97be-4bf7853e319f.png b/apps/kb/public/img/troubleshooting/49e175d9-6338-4b17-97be-4bf7853e319f.png new file mode 100644 index 00000000000..cf96e93ce90 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/49e175d9-6338-4b17-97be-4bf7853e319f.png differ diff --git a/apps/kb/public/img/troubleshooting/54428354-a1f4-4c44-bd03-d87f4b925d94.png b/apps/kb/public/img/troubleshooting/54428354-a1f4-4c44-bd03-d87f4b925d94.png new file mode 100644 index 00000000000..ea112beea61 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/54428354-a1f4-4c44-bd03-d87f4b925d94.png differ diff --git a/apps/kb/public/img/troubleshooting/546_chart.png b/apps/kb/public/img/troubleshooting/546_chart.png new file mode 100644 index 00000000000..59d11c0a059 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/546_chart.png differ diff --git a/apps/kb/public/img/troubleshooting/546_errors_by_hour.png b/apps/kb/public/img/troubleshooting/546_errors_by_hour.png new file mode 100644 index 00000000000..dcb97dfa03d Binary files /dev/null and b/apps/kb/public/img/troubleshooting/546_errors_by_hour.png differ diff --git a/apps/kb/public/img/troubleshooting/565c96e0-cbb9-4e7a-b0aa-5b423afc0ada.png b/apps/kb/public/img/troubleshooting/565c96e0-cbb9-4e7a-b0aa-5b423afc0ada.png new file mode 100644 index 00000000000..2b0270cbfef Binary files /dev/null and b/apps/kb/public/img/troubleshooting/565c96e0-cbb9-4e7a-b0aa-5b423afc0ada.png differ diff --git a/apps/kb/public/img/troubleshooting/5b55ddbc-abfc-4c66-a012-b8e4d23cd438.png b/apps/kb/public/img/troubleshooting/5b55ddbc-abfc-4c66-a012-b8e4d23cd438.png new file mode 100644 index 00000000000..f8742c40f46 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/5b55ddbc-abfc-4c66-a012-b8e4d23cd438.png differ diff --git a/apps/kb/public/img/troubleshooting/627c1a3e-153e-40b5-9a91-a4f7bd7673a4.png b/apps/kb/public/img/troubleshooting/627c1a3e-153e-40b5-9a91-a4f7bd7673a4.png new file mode 100644 index 00000000000..81c666ad573 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/627c1a3e-153e-40b5-9a91-a4f7bd7673a4.png differ diff --git a/apps/kb/public/img/troubleshooting/6c284180-0ffd-432d-b86b-e9fbcfe23868.png b/apps/kb/public/img/troubleshooting/6c284180-0ffd-432d-b86b-e9fbcfe23868.png new file mode 100644 index 00000000000..ddba69bc5b2 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/6c284180-0ffd-432d-b86b-e9fbcfe23868.png differ diff --git a/apps/kb/public/img/troubleshooting/7fe991ac-40ea-4628-a5e3-181d706225ea.png b/apps/kb/public/img/troubleshooting/7fe991ac-40ea-4628-a5e3-181d706225ea.png new file mode 100644 index 00000000000..8b04c2bf89a Binary files /dev/null and b/apps/kb/public/img/troubleshooting/7fe991ac-40ea-4628-a5e3-181d706225ea.png differ diff --git a/apps/kb/public/img/troubleshooting/800448a1-0245-4a26-855f-3be8f44adf03.png b/apps/kb/public/img/troubleshooting/800448a1-0245-4a26-855f-3be8f44adf03.png new file mode 100644 index 00000000000..2296a7fa06a Binary files /dev/null and b/apps/kb/public/img/troubleshooting/800448a1-0245-4a26-855f-3be8f44adf03.png differ diff --git a/apps/kb/public/img/troubleshooting/830b81aa-ae52-44a4-aa49-32c2a6296af7.png b/apps/kb/public/img/troubleshooting/830b81aa-ae52-44a4-aa49-32c2a6296af7.png new file mode 100644 index 00000000000..626d84d8fce Binary files /dev/null and b/apps/kb/public/img/troubleshooting/830b81aa-ae52-44a4-aa49-32c2a6296af7.png differ diff --git a/apps/kb/public/img/troubleshooting/8ba54441-2057-4dc0-8add-a8eaab9f2d93.png b/apps/kb/public/img/troubleshooting/8ba54441-2057-4dc0-8add-a8eaab9f2d93.png new file mode 100644 index 00000000000..bcede723b46 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/8ba54441-2057-4dc0-8add-a8eaab9f2d93.png differ diff --git a/apps/kb/public/img/troubleshooting/90cb6a70-fc67-4666-9217-6fa37cfacf82.png b/apps/kb/public/img/troubleshooting/90cb6a70-fc67-4666-9217-6fa37cfacf82.png new file mode 100644 index 00000000000..b7f8c7c5950 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/90cb6a70-fc67-4666-9217-6fa37cfacf82.png differ diff --git a/apps/kb/public/img/troubleshooting/9b88a738-65d6-4be4-8fa0-bc3f13a9a408.png b/apps/kb/public/img/troubleshooting/9b88a738-65d6-4be4-8fa0-bc3f13a9a408.png new file mode 100644 index 00000000000..e8600fb6064 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/9b88a738-65d6-4be4-8fa0-bc3f13a9a408.png differ diff --git a/apps/kb/public/img/troubleshooting/9c00830a-8901-abcd-ef12-34567890abcd.png b/apps/kb/public/img/troubleshooting/9c00830a-8901-abcd-ef12-34567890abcd.png new file mode 100644 index 00000000000..fc4b16a8a6f Binary files /dev/null and b/apps/kb/public/img/troubleshooting/9c00830a-8901-abcd-ef12-34567890abcd.png differ diff --git a/apps/kb/public/img/troubleshooting/9c6e0f9c-f842-463b-b214-1a9e31de3b2c.png b/apps/kb/public/img/troubleshooting/9c6e0f9c-f842-463b-b214-1a9e31de3b2c.png new file mode 100644 index 00000000000..033951c6a4f Binary files /dev/null and b/apps/kb/public/img/troubleshooting/9c6e0f9c-f842-463b-b214-1a9e31de3b2c.png differ diff --git a/apps/kb/public/img/troubleshooting/a360b417-e0cc-4706-8df4-89af63dcdc70.png b/apps/kb/public/img/troubleshooting/a360b417-e0cc-4706-8df4-89af63dcdc70.png new file mode 100644 index 00000000000..c8a0bebbf7a Binary files /dev/null and b/apps/kb/public/img/troubleshooting/a360b417-e0cc-4706-8df4-89af63dcdc70.png differ diff --git a/apps/kb/public/img/troubleshooting/ade24c85-50b5-47ca-a2ce-c3f77a0e128a.png b/apps/kb/public/img/troubleshooting/ade24c85-50b5-47ca-a2ce-c3f77a0e128a.png new file mode 100644 index 00000000000..7c7accbc092 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/ade24c85-50b5-47ca-a2ce-c3f77a0e128a.png differ diff --git a/apps/kb/public/img/troubleshooting/c4e7b193-4567-8901-abcd-ef1234567890.png b/apps/kb/public/img/troubleshooting/c4e7b193-4567-8901-abcd-ef1234567890.png new file mode 100644 index 00000000000..10649da3517 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/c4e7b193-4567-8901-abcd-ef1234567890.png differ diff --git a/apps/kb/public/img/troubleshooting/c6e228b8-fa64-47a9-bdf6-f1f6405b9cf9.png b/apps/kb/public/img/troubleshooting/c6e228b8-fa64-47a9-bdf6-f1f6405b9cf9.png new file mode 100644 index 00000000000..cd27655527a Binary files /dev/null and b/apps/kb/public/img/troubleshooting/c6e228b8-fa64-47a9-bdf6-f1f6405b9cf9.png differ diff --git a/apps/kb/public/img/troubleshooting/c7ac0f69-6fb9-4ff2-8aab-5cda6bada3f6.png b/apps/kb/public/img/troubleshooting/c7ac0f69-6fb9-4ff2-8aab-5cda6bada3f6.png new file mode 100644 index 00000000000..11ea0ee5608 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/c7ac0f69-6fb9-4ff2-8aab-5cda6bada3f6.png differ diff --git a/apps/kb/public/img/troubleshooting/cf450bca-aad3-4783-ae3b-5cc83e0afa54.png b/apps/kb/public/img/troubleshooting/cf450bca-aad3-4783-ae3b-5cc83e0afa54.png new file mode 100644 index 00000000000..19c5a6b1aec Binary files /dev/null and b/apps/kb/public/img/troubleshooting/cf450bca-aad3-4783-ae3b-5cc83e0afa54.png differ diff --git a/apps/kb/public/img/troubleshooting/d756aeb0-515f-425d-b737-75a935935b73.png b/apps/kb/public/img/troubleshooting/d756aeb0-515f-425d-b737-75a935935b73.png new file mode 100644 index 00000000000..10fb585b240 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/d756aeb0-515f-425d-b737-75a935935b73.png differ diff --git a/apps/kb/public/img/troubleshooting/da70bc33-f123-4567-8901-abcdef123456.png b/apps/kb/public/img/troubleshooting/da70bc33-f123-4567-8901-abcdef123456.png new file mode 100644 index 00000000000..2cea7ebe885 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/da70bc33-f123-4567-8901-abcdef123456.png differ diff --git a/apps/kb/public/img/troubleshooting/download-edge-function-via-dashboard.gif b/apps/kb/public/img/troubleshooting/download-edge-function-via-dashboard.gif new file mode 100644 index 00000000000..58f19698fa0 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/download-edge-function-via-dashboard.gif differ diff --git a/apps/kb/public/img/troubleshooting/e7d78109-b09d-48ca-8cc6-e4913693e163.png b/apps/kb/public/img/troubleshooting/e7d78109-b09d-48ca-8cc6-e4913693e163.png new file mode 100644 index 00000000000..b75cf3c3b3a Binary files /dev/null and b/apps/kb/public/img/troubleshooting/e7d78109-b09d-48ca-8cc6-e4913693e163.png differ diff --git a/apps/kb/public/img/troubleshooting/e8f50ef1-2b3c-4d5e-9f8a-1234567890ab.png b/apps/kb/public/img/troubleshooting/e8f50ef1-2b3c-4d5e-9f8a-1234567890ab.png new file mode 100644 index 00000000000..50b480e0cc8 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/e8f50ef1-2b3c-4d5e-9f8a-1234567890ab.png differ diff --git a/apps/kb/public/img/troubleshooting/ec5ed538-985b-411d-a0d7-be718b28d367.png b/apps/kb/public/img/troubleshooting/ec5ed538-985b-411d-a0d7-be718b28d367.png new file mode 100644 index 00000000000..4273e62922a Binary files /dev/null and b/apps/kb/public/img/troubleshooting/ec5ed538-985b-411d-a0d7-be718b28d367.png differ diff --git a/apps/kb/public/img/troubleshooting/edge-function-504-timestamp-1.png b/apps/kb/public/img/troubleshooting/edge-function-504-timestamp-1.png new file mode 100644 index 00000000000..5b246a57c36 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/edge-function-504-timestamp-1.png differ diff --git a/apps/kb/public/img/troubleshooting/edge_function_404_redeploy.png b/apps/kb/public/img/troubleshooting/edge_function_404_redeploy.png new file mode 100644 index 00000000000..04f971711b4 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/edge_function_404_redeploy.png differ diff --git a/apps/kb/public/img/troubleshooting/edge_function_404_set_timeframe.png b/apps/kb/public/img/troubleshooting/edge_function_404_set_timeframe.png new file mode 100644 index 00000000000..57465225be1 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/edge_function_404_set_timeframe.png differ diff --git a/apps/kb/public/img/troubleshooting/edge_function_404_test_call.png b/apps/kb/public/img/troubleshooting/edge_function_404_test_call.png new file mode 100644 index 00000000000..85fd737effc Binary files /dev/null and b/apps/kb/public/img/troubleshooting/edge_function_404_test_call.png differ diff --git a/apps/kb/public/img/troubleshooting/edge_functions_401_verify_jwt.png b/apps/kb/public/img/troubleshooting/edge_functions_401_verify_jwt.png new file mode 100644 index 00000000000..dc8ca2b3037 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/edge_functions_401_verify_jwt.png differ diff --git a/apps/kb/public/img/troubleshooting/f294a4d6-ef12-3456-7890-abcdef123456.png b/apps/kb/public/img/troubleshooting/f294a4d6-ef12-3456-7890-abcdef123456.png new file mode 100644 index 00000000000..7761da27ba5 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/f294a4d6-ef12-3456-7890-abcdef123456.png differ diff --git a/apps/kb/public/img/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.jpg b/apps/kb/public/img/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.jpg new file mode 100644 index 00000000000..05ac0011045 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/failed-to-run-sql-query-connection-terminated-due-to-connection-timeout.jpg differ diff --git a/apps/kb/public/img/troubleshooting/ff01bba8-abcd-ef12-3456-7890abcdef12.png b/apps/kb/public/img/troubleshooting/ff01bba8-abcd-ef12-3456-7890abcdef12.png new file mode 100644 index 00000000000..c781daef2bf Binary files /dev/null and b/apps/kb/public/img/troubleshooting/ff01bba8-abcd-ef12-3456-7890abcdef12.png differ diff --git a/apps/kb/public/img/troubleshooting/filter_503.png b/apps/kb/public/img/troubleshooting/filter_503.png new file mode 100644 index 00000000000..efa0f647959 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/filter_503.png differ diff --git a/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-0.png b/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-0.png new file mode 100644 index 00000000000..5fbc641c570 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-0.png differ diff --git a/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-1.png b/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-1.png new file mode 100644 index 00000000000..c2eea9a364d Binary files /dev/null and b/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-1.png differ diff --git a/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-2.png b/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-2.png new file mode 100644 index 00000000000..68fcfa2c9b5 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/identify-lovable-cloud-or-supabase-backend-2.png differ diff --git a/apps/kb/public/img/troubleshooting/limit_in_logs.png b/apps/kb/public/img/troubleshooting/limit_in_logs.png new file mode 100644 index 00000000000..f4a4524453c Binary files /dev/null and b/apps/kb/public/img/troubleshooting/limit_in_logs.png differ diff --git a/apps/kb/public/img/troubleshooting/limit_logs.png b/apps/kb/public/img/troubleshooting/limit_logs.png new file mode 100644 index 00000000000..afde11ed385 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/limit_logs.png differ diff --git a/apps/kb/public/img/troubleshooting/migrations-failed-status.png b/apps/kb/public/img/troubleshooting/migrations-failed-status.png new file mode 100644 index 00000000000..0b4a86184b4 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/migrations-failed-status.png differ diff --git a/apps/kb/public/img/troubleshooting/postgres-logs-migration-failed.png b/apps/kb/public/img/troubleshooting/postgres-logs-migration-failed.png new file mode 100644 index 00000000000..d2e8c7b44ab Binary files /dev/null and b/apps/kb/public/img/troubleshooting/postgres-logs-migration-failed.png differ diff --git a/apps/kb/public/img/troubleshooting/project-status-unhealthy-services.png b/apps/kb/public/img/troubleshooting/project-status-unhealthy-services.png new file mode 100644 index 00000000000..0cfa6ca94cd Binary files /dev/null and b/apps/kb/public/img/troubleshooting/project-status-unhealthy-services.png differ diff --git a/apps/kb/public/img/troubleshooting/query_results_546.png b/apps/kb/public/img/troubleshooting/query_results_546.png new file mode 100644 index 00000000000..bc37b3f8113 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/query_results_546.png differ diff --git a/apps/kb/public/img/troubleshooting/rebase-image.png b/apps/kb/public/img/troubleshooting/rebase-image.png new file mode 100644 index 00000000000..f164eb83486 Binary files /dev/null and b/apps/kb/public/img/troubleshooting/rebase-image.png differ diff --git a/apps/kb/public/img/troubleshooting/reports-page-egress.png b/apps/kb/public/img/troubleshooting/reports-page-egress.png new file mode 100644 index 00000000000..8801fc6fd8e Binary files /dev/null and b/apps/kb/public/img/troubleshooting/reports-page-egress.png differ diff --git a/apps/kb/public/img/troubleshooting/upload-edge-function-via-dashboard.gif b/apps/kb/public/img/troubleshooting/upload-edge-function-via-dashboard.gif new file mode 100644 index 00000000000..b8835bb201b Binary files /dev/null and b/apps/kb/public/img/troubleshooting/upload-edge-function-via-dashboard.gif differ diff --git a/apps/kb/scripts/federated-content/backfill-legacy-slugs.ts b/apps/kb/scripts/federated-content/backfill-legacy-slugs.ts new file mode 100644 index 00000000000..db62d761fb8 --- /dev/null +++ b/apps/kb/scripts/federated-content/backfill-legacy-slugs.ts @@ -0,0 +1,63 @@ +// One-time migration aid: troubleshooting_entries has 278 rows pre-dating kb +// (apps/docs' own discussions), all with slug = NULL. legacy-troubleshooting-slugs.json +// maps each row's id (docs' own `database_id` frontmatter field) to the slug +// of the docs content file it came from, extracted once from +// apps/docs/content/troubleshooting/*.mdx (that content isn't checked out by +// this workflow and doesn't need to be — these ids never change). +// +// Stored as the bare slug, not prefixed: as these files get migrated into the +// federated supabase/troubleshooting repo under the same slug, sync-troubleshooting-entries.ts +// needs to match them by slug against these rows' existing github_url — otherwise +// it treats the guide as new and creates a duplicate discussion, orphaning the +// original (with its comments and links). +// +// Delete this file, legacy-troubleshooting-slugs.json, its package.json script, +// and its workflow step once all legacy rows are backfilled — safe to remove +// as a unit, nothing else depends on it. +import '../utils/dotenv.js' + +import { createClient } from '@supabase/supabase-js' + +import legacySlugs from './legacy-troubleshooting-slugs.json' with { type: 'json' } + +const DRY_RUN = process.argv.includes('--dry-run') + +async function backfillLegacySlugs() { + if (DRY_RUN) { + for (const [databaseId, slug] of Object.entries(legacySlugs)) { + console.log(` - ${databaseId} -> ${slug}`) + } + return + } + + const db = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string + ) + + let backfilled = 0 + let alreadyDone = 0 + + for (const [databaseId, slug] of Object.entries(legacySlugs)) { + // .is('slug', null) makes this idempotent/concurrency-safe: a duplicate + // run updates 0 rows instead of racing. + const { data, error } = await db + .from('troubleshooting_entries') + .update({ slug }) + .eq('id', databaseId) + .is('slug', null) + .select('id') + if (error) throw error + + if (data.length > 0) backfilled++ + else alreadyDone++ + } + + console.log( + `[backfill-legacy-slugs] Backfilled ${backfilled}, already done ${alreadyDone}, total ${Object.keys(legacySlugs).length}.` + ) +} + +backfillLegacySlugs().catch((error) => { + throw error +}) diff --git a/apps/kb/scripts/federated-content/fetch-federated-content.ts b/apps/kb/scripts/federated-content/fetch-federated-content.ts new file mode 100644 index 00000000000..35bc1db748a --- /dev/null +++ b/apps/kb/scripts/federated-content/fetch-federated-content.ts @@ -0,0 +1,118 @@ +// Fetches every troubleshooting article from the private supabase/troubleshooting +// repo's `guides` folder into src/content/troubleshooting/ (gitignored, +// regenerated on every prebuild — see content.config.ts's `troubleshooting` +// collection). No content processing: body text is written as fetched. +import '../utils/dotenv.js' + +import { mkdir, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { Octokit } from '@octokit/core' +import { retry } from '@octokit/plugin-retry' +import matter from 'gray-matter' + +import { githubAuthOptions } from './github-auth.ts' + +const SOURCE = { org: 'supabase', repo: 'troubleshooting', branch: 'main', dir: 'guides' } + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const TROUBLESHOOTING_DIRECTORY = join(SCRIPT_DIR, '../../src/content/troubleshooting') + +const RetryOctokit = Octokit.plugin(retry) +let octokitInstance: InstanceType + +function octokit() { + if (!octokitInstance) octokitInstance = new RetryOctokit(githubAuthOptions()) + return octokitInstance +} + +async function listMarkdownFiles(path: string): Promise { + const response = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', { + owner: SOURCE.org, + repo: SOURCE.repo, + path, + ref: SOURCE.branch, + }) + if (!Array.isArray(response.data)) + throw new Error(`${path} in ${SOURCE.org}/${SOURCE.repo} is not a directory`) + return response.data + .filter((entry) => entry.type === 'file' && /\.mdx?$/.test(entry.name)) + .map((entry) => entry.name) +} + +async function getFileContents(path: string): Promise { + const response = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', { + owner: SOURCE.org, + repo: SOURCE.repo, + path, + ref: SOURCE.branch, + }) + if ( + Array.isArray(response.data) || + !('content' in response.data) || + response.data.type !== 'file' + ) { + throw new Error(`Unexpected response for ${path} in ${SOURCE.org}/${SOURCE.repo}`) + } + return Buffer.from(response.data.content, 'base64').toString('utf-8') +} + +// Local slug: strip the extension and the random hex suffix some filenames +// carry (e.g. `foo-bar-94a76b.mdx` -> `foo-bar`). +function slugFromFilename(filename: string): string { + return filename.replace(/\.mdx?$/, '').replace(/-[0-9a-f]{6}$/i, '') +} + +// Most source files use TOML frontmatter (`title = "..."`), which gray-matter +// can't parse (it assumes YAML) — it either throws or silently returns +// garbage. Detected here by gray-matter failing to produce a `title` string, +// and skipped entirely rather than guessed at. +function parseGuide(raw: string): { title: string; topics: string[]; body: string } | null { + let data: Record + let body: string + try { + ;({ data, content: body } = matter(raw)) + } catch { + return null + } + if (typeof data.title !== 'string') return null + return { title: data.title, topics: Array.isArray(data.topics) ? data.topics : [], body } +} + +async function fetchPage(filename: string): Promise<'fetched' | 'legacy-toml'> { + const raw = await getFileContents(`${SOURCE.dir}/${filename}`) + const guide = parseGuide(raw) + if (!guide) return 'legacy-toml' + + const frontmatter = { + title: guide.title, + topics: guide.topics, + } + const output = matter.stringify(guide.body, frontmatter) + await writeFile(join(TROUBLESHOOTING_DIRECTORY, `${slugFromFilename(filename)}.md`), output) + return 'fetched' +} + +async function fetchFederatedContent() { + await mkdir(TROUBLESHOOTING_DIRECTORY, { recursive: true }) + + const filenames = await listMarkdownFiles(SOURCE.dir) + const results = await Promise.all(filenames.map((filename) => fetchPage(filename))) + + const legacyTomlFiles = filenames.filter((_, i) => results[i] === 'legacy-toml') + if (legacyTomlFiles.length > 0) { + console.warn( + `[fetch-federated-content.ts]: ${legacyTomlFiles.length} troubleshooting guide(s) contained TOML frontmatter syntax and were ignored:` + ) + for (const filename of legacyTomlFiles) console.warn(` - ${filename}`) + } + + const fetchedCount = filenames.length - legacyTomlFiles.length + console.log( + `Fetched ${fetchedCount}/${filenames.length} troubleshooting page(s) into src/content/troubleshooting/` + ) +} + +fetchFederatedContent().catch((error) => { + throw error +}) diff --git a/apps/kb/scripts/federated-content/github-auth.ts b/apps/kb/scripts/federated-content/github-auth.ts new file mode 100644 index 00000000000..41c5f1bddc0 --- /dev/null +++ b/apps/kb/scripts/federated-content/github-auth.ts @@ -0,0 +1,44 @@ +import crypto from 'node:crypto' +import { createAppAuth } from '@octokit/auth-app' + +/** + * Prefers the docs GitHub App. Falls back to `GH_TOKEN` / `GITHUB_TOKEN` for + * local runs. A partially configured App is an error rather than a token + * fall-back, to avoid silently authenticating as the wrong identity. + */ +export function githubAuthOptions() { + const appId = process.env.KB_GITHUB_APP_ID + const installationId = process.env.KB_GITHUB_APP_INSTALLATION_ID + const privateKey = process.env.KB_GITHUB_APP_PRIVATE_KEY + + if (appId && installationId && privateKey) { + return { + authStrategy: createAppAuth, + auth: { + appId, + installationId, + privateKey: crypto + .createPrivateKey(privateKey) + .export({ type: 'pkcs8', format: 'pem' }) + .toString(), + }, + } + } + + const appVars: Array<[string, string | undefined]> = [ + ['KB_GITHUB_APP_ID', appId], + ['KB_GITHUB_APP_INSTALLATION_ID', installationId], + ['KB_GITHUB_APP_PRIVATE_KEY', privateKey], + ] + const missing = appVars.filter(([, value]) => !value).map(([name]) => name) + if (missing.length < appVars.length) { + throw new Error(`Incomplete GitHub App configuration: ${missing.join(', ')} not set.`) + } + + const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN + if (token) return { auth: token } + + throw new Error( + 'Missing GitHub credentials. Set KB_GITHUB_APP_ID/_INSTALLATION_ID/_PRIVATE_KEY, or GH_TOKEN / GITHUB_TOKEN.' + ) +} diff --git a/apps/kb/scripts/federated-content/legacy-troubleshooting-slugs.json b/apps/kb/scripts/federated-content/legacy-troubleshooting-slugs.json new file mode 100644 index 00000000000..bee60c2b163 --- /dev/null +++ b/apps/kb/scripts/federated-content/legacy-troubleshooting-slugs.json @@ -0,0 +1,172 @@ +{ + "97923126-7d0b-4bf0-bb65-55dae289a8a3": "42501--permission-denied-for-table-httprequestqueue-KnozmQ", + "ba593989-12b5-464f-8ede-a525e1ca2ffb": "all-about-supabase-egress-a_Sg_e", + "7183351e-dba3-484d-83b3-bb89a027e36a": "an-invalid-response-was-received-from-the-upstream-server-error-when-querying-auth-RI4Vl-", + "ce8c04e4-d493-4e15-832a-e59bdcf2b093": "app-store-rejection-tls-error-in-ipv6-only-environments-4e9c62", + "03854567-8838-4f12-8a6c-095fc1671d9f": "are-all-features-available-in-self-hosted-supabase-THPcqw", + "e90ca9e0-0376-4313-b32b-b9a4ae81bf7a": "auth-error-401-invalid-claim-missing-sub--AFwMR", + "c1de4561-e95f-41e3-b298-fac9ae331a54": "auth-hooks-invalid-payload-when-anonymous-users-attempt-phone-changes-022c47", + "a931b8af-3210-4188-bb03-87452923a498": "autovacuum-stalled-due-to-inactive-replication-slot-d55aa2", + "8bb130d5-d298-49c7-8cfb-dd7a3f231154": "avoiding-timeouts-in-long-running-queries-6nmbdN", + "4a41e734-6883-452f-bc8e-82ff4ac22a81": "canceling-statement-due-to-statement-timeout-581wFv", + "f7f2d68b-f336-4578-9541-17ca37aaa327": "cant-access-supabase-project-lovable-cloud", + "df9518df-d019-42bb-8d91-918c6d57b452": "certain-operations-are-too-complex-to-perform-directly-using-the-client-libraries-8JaphH", + "6de3c3a9-8614-48d7-827e-cf41d6a1ce23": "change-email-associated-with-supabase-account-T5eHNT", + "fd711d33-9703-402f-84b2-8e74034e7690": "change-project-region-eWJo5Z", + "1759134d-a98d-4bfa-9501-71e06180700e": "check-usage-for-monthly-active-users-mau-MwZaBs", + "a60ee728-3add-438d-b8bf-433f1746cb3e": "cloudflare-origin-error-1016-on-custom-domain-a57af4", + "94c6e33b-cc05-49b3-b010-7480f8dde843": "converted-github-account-to-organisation---lost-supabase-account-access-5wsE_1", + "2ebc2849-0cf7-4c94-ac13-22167a196a92": "customizing-emails-by-language-KZ_38Q", + "c5d8260b-c17a-4f6c-ab20-801f953f6c94": "dashboard-errors-when-managing-users-N1ls4A", + "49b51a3a-9753-4747-a24e-8afcb075792b": "database-api-42501-errors", + "b3a73631-0b8d-460d-8033-28df88d26d75": "database-error-remaining-connection-slots-are-reserved-for-non-replication-superuser-connections-3V3nIb", + "5d1c44ed-b2f6-4509-9312-1eeb8838e701": "database-error-saving-new-user-RU_EwB", + "58c0cb7c-50a0-4a96-9551-bc97c28b7393": "deprecated-rls-features-Pm77Zs", + "04801b69-e7eb-4f40-8d41-81110397bbc2": "disabling-prepared-statements-qL8lEL", + "188986c9-019d-4f26-baaf-6f58cec8fa7a": "discovering-and-interpreting-api-errors-in-the-logs-7xREI9", + "1e02e989-fc12-4838-b304-c7d1356f6d2c": "do-i-need-to-expose-security-definer-functions-in-row-level-security-policies-iI0uOw", + "91ae4c49-bf22-4b8c-b96b-cd11f1c38158": "download-logical-backups", + "a3cfff33-d640-4395-99bc-a110b7fb637c": "edge-function-404-error-response", + "4e6ff2e4-2abb-4fba-8233-5883b3d56fb0": "edge-function-546-error-response", + "aaf9e673-64ae-460a-88e0-b83ea4963382": "edge-function-bundle-size-issues", + "1765884f-81d6-415a-a78f-7085f7b7ddbf": "edge-function-cpu-limits", + "e079a9d0-419a-4e31-b7ec-1206d9012d0b": "edge-function-dependency-analysis", + "30b3f7a4-a2b7-4430-8176-31aa4d92b2e9": "edge-function-fails-deploy", + "33e8e407-951b-4f7d-b8b4-8e9085cd4d10": "edge-function-monitoring-resource-usage", + "109b964f-c28c-4554-b059-cd8b165c63f8": "edge-function-shutdown-reasons-explained", + "89b868a9-17fe-4c6d-86f6-0b04e5794678": "edge-function-takes-too-long-to-respond", + "5a3a0125-84c7-463b-a886-ca53edfba5f4": "edge-function-wall-clock-time-limit-reached-Nk38bW", + "f988e832-b0c3-4b30-8e20-8d5854943b32": "email-password-login-disabled-supabase-vercel-marketplace-a7dd36", + "47c4e2c7-0ff3-48e7-8c8a-912494ff8c24": "email-template-not-updating", + "02f5f200-3c1c-425e-945f-b80b3fce3ec0": "enabling-ipv4-addon", + "9d8b179c-ff4d-4072-b9e6-2d16e63b2aaa": "error-connection-refused-when-trying-to-connect-to-supabase-database-hwG0Dr", + "da3aa582-b115-44e2-97e3-9ab76206f0db": "error-index-row-size-exceeds-btree-version-4-maximum-for-index-LMmoeU", + "12128731-0af6-4c57-9aa2-66e177f0c3f4": "error-invalid-byte-sequence-for-encoding-utf8-0x00-when-accessing-triggers-or-webhooks-e78cf8", + "58d5a2de-2f7e-45fc-8dfa-7fb36da41ca8": "error-invalid-totp-code-entered-CukLCj", + "0739256c-1b25-46ec-963b-2f8e4dccedc7": "error-no-pghbaconf-entry-for-host-xxxxxxxxxxx-user-postgres-database-postgres-ssl-off-GOt5Ja", + "8688dad6-3dce-4912-9b90-4f128daf79d7": "error-prepared-statement-xxx-already-exists-3laqeM", + "6083c9eb-b9fc-4d19-bc64-bb4b7efeecdd": "error-target-organization-is-not-managed-by-vercel-marketplace-during-project-transfer-5262c3", + "4844905d-1456-44a1-858e-7a4995e5054c": "exhaust-disk-io", + "a589d411-ee50-455a-892e-526914ddb40d": "exhaust-ram", + "2db43a09-792b-484b-b97d-5d96a405b525": "exhaust-swap", + "23c4e1b5-c7e7-497f-acf8-006133e243a4": "failed-to-fetch-in-dashboard-and-other-areas----browser-extension-dyDTRU", + "aa5fda82-5838-4975-b0ac-ba4d7fb9e638": "failed-to-restore-from-backup-all-subscriptions-and-replication-slots-must-be-dropped-before-a-backup-can-be-restored-L-rCvt", + "095da77f-b97f-455c-88d1-b6a0341452b1": "failed-to-retrieve-tables", + "7529ca12-114b-485c-9b64-bca3b2dc744b": "failed-to-run-sql-query-connection-terminated-due-to-connection-timeout", + "6f19b113-09cb-45ce-8141-0d6dc10ea464": "fetch-requests-to-api-endpoints-arent-showing-the-session-UbUwRs", + "6ec0c9a3-b52b-49b9-ba6e-c91984f7f9bf": "fixing-520-errors-in-the-database-rest-api-Ur5-B2", + "b095c3ed-23e7-4a17-9e33-d811af25ba67": "forbidden-resource-error-from-the-cli-L6rm6l", + "2c33968e-26c0-4613-aee1-5de36d068394": "get-detailed-storage-metrics-with-the-aws-cli-587a7d", + "1ea7b605-dc1d-43d8-a365-5bc2801dd90e": "google-auth-fails-for-some-users-XcFXEu", + "76a4099e-450f-4b5b-a539-224760348c18": "grafana-not-displaying-data-sXJrMj", + "23668386-7a72-44ff-a412-4cd8a005fa18": "high-cpu-and-slow-queries-with-error-must-be-a-superuser-to-terminate-superuser-process", + "3d1bb799-453f-4a6c-9fdc-056f15bec88c": "high-cpu-usage", + "a715c0d3-8636-41b7-a6da-6d49f1b1777c": "high-latency-with-supabase-client-z0pZzR", + "b7edb30b-beee-40ae-9b4f-666ef6411bb6": "how-can-i-revoke-execution-of-a-postgresql-function-2GYb0A", + "74972531-f2fc-4d68-a745-21f9c75761e6": "how-do-i-check-gotrueapi-version-of-a-supabase-project-lQAnOR", + "cdaa76a4-3e5b-4c8f-82f0-b713c4cf19ab": "how-do-i-make-the-cookies-httponly-vwweFx", + "6e4a515c-5e70-4b92-808c-f076d47d4e35": "how-do-i-reset-my-supabase-database-password-oTs5sB", + "dfefd00c-31d8-4969-86a4-03d586110f4c": "how-do-i-update-connection-pool-settings-in-my-dashboard-wAxTJ_", + "4017d331-0b24-439a-9231-793801395ca2": "how-do-you-troubleshoot-nextjs---supabase-auth-issues-riMCZV", + "cc852935-510f-4beb-9f70-b4582505a863": "how-long-does-it-take-to-restore-a-database-from-a-point-in-time-backup-pitr-qO8gOG", + "e7671f20-2639-442e-9df9-4f6fa3766598": "how-postgres-chooses-which-index-to-use-_JHrf4", + "6f6a34a6-ac3f-4fba-9c70-4f4b8c7aeda7": "how-to-bypass-cooldown-period", + "78f06ca5-6a69-4536-89fc-ed2e6ff334e5": "how-to-change-max-database-connections-_BQ8P5", + "990ed738-2e00-42ce-9892-77c16f870cdc": "how-to-check-if-my-queries-are-being-blocked-by-other-queries-NSKtR1", + "085d7649-0b5d-4c3c-b6a1-f2da4b9f33dd": "how-to-delete-a-role-in-postgres-8-AvxY", + "aafe6f52-de09-44ec-b329-1bbbf26146dd": "how-to-delete-vercel-linked-projects-9d08aa", + "8b000bb4-180b-4a6c-b280-ba02965060f6": "how-to-interpret-and-explore-the-postgres-logs-OuCIOj", + "273b79ad-b8ff-4669-8544-b99920e2d3c8": "how-to-migrate-from-supabase-auth-helpers-to-ssr-package-5NRunM", + "da2d95e5-abc5-47c8-8389-1554d12abf91": "how-to-view-database-metrics-uqf2z_", + "e9fb853f-a37f-442a-b180-4aefd234a385": "http-api-issues", + "b98c7288-8c38-4dc2-8591-d18ea9eb4cbc": "http-status-codes", + "ec816bde-1a27-461f-89ef-211e96810dd9": "i-am-not-receiving-password-reset-emails-for-supabase-dashboard--cO5yf", + "37244f4a-cf99-4b1d-b7f2-ee70c3a126ab": "identify-lovable-cloud-or-supabase-backend", + "225cb141-f442-47cf-834d-1d82d703748a": "importing-stripe-or-other-modules-from-esmsh-on-deno-edge-functions-throws-an-error-TmbB5p", + "7d755701-747f-4c3a-b8be-236c5518e4eb": "increase-vector-lookup-speeds-by-applying-an-hsnw-index-ohLHUM", + "a1156bbe-289e-4462-a32d-2b0ca03691e4": "inserting-into-sequenceserial-table-causes-duplicate-key-violates-unique-constraint-error-pi6DnC", + "f47c9b0e-f537-4908-8d35-5dd3dc4c5627": "inspecting-edge-function-environment-variables-wg5qOQ", + "1da315be-38ac-487a-b178-f865a1a73c09": "insufficient-privilege-when-accessing-pgstatstatements-e5M_EQ", + "ef05da0a-f8bc-44a4-9719-5ae811dba104": "interpreting-supabase-grafana-cpu-charts-9JSlkC", + "0056cd40-df04-4045-bbfb-c245cb15b85d": "interpreting-supabase-grafana-io-charts-MUynDR", + "1cff12df-7ad6-48c5-b518-5ea468b54bab": "issues-serving-edge-functions-locally", + "362840be-a99c-4a98-bf4b-6e44a399af12": "jwt-expired-error-in-supabase-dashboard-F06k3x", + "449482fb-6e21-4a25-99ee-1ce6fffbc975": "keeping-free-projects-after-pro-upgrade-Kf9Xm2", + "60bd4a35-fc2e-4e42-bcf6-a430d380c930": "lost-accessforgot-the-mfa-device-nAPT-7", + "f6420e72-ea67-4825-b3f7-e722ea5c0d96": "manually-created-databases-are-not-visible-in-the-supabase-dashboard-4415aa", + "43b62743-252c-4069-a719-0c0737839fd9": "memory-and-swap-usage-explained-aPNgm0", + "7bd3de18-a4ab-4b76-85e4-b72924dacd2a": "migrating-auth-users-between-projects", + "bdd39473-ae02-41f5-b75d-877a79f361f6": "monitor-supavisor-postgres-connections", + "c5dae428-8dd0-4879-b312-df7f7da25986": "new-branch-doesnt-copy-database", + "f5cabb97-aae3-4fb5-b597-4e7857821753": "new-variable-is-null-in-a-trigger-function--l9AOZ", + "9be3727b-c3ea-4dda-b30c-55f3c8aa8f35": "nextjs-1314-stale-data-when-changing-rls-or-table-data-85b8oQ", + "a1bb1131-dd11-4a76-8a70-98a666c08286": "no-toast-messages-on-the-dashboard-BrvP8h", + "98e3b318-ad9a-41f5-ada0-44b61970ab4f": "not-receiving-auth-emails-from-the-supabase-project-OFSNzw", + "3e9246cb-d592-4051-93c8-53e4e555711c": "oauth-sign-in-isnt-redirecting-on-the-server-side-ShGMtr", + "25eddb73-3cca-485b-b87f-7279dd46b7a7": "otp-verification-failures-token-has-expired-or-otp_expired-errors-5ee4d0", + "3142f418-028e-4ab1-8f82-a7096be90422": "partitioning-an-existing-table-with-same-name-VEnbzK", + "76b2e2b6-8f1d-448a-a3dd-cdf68755d1a9": "pausing-pro-projects-vNL-2a", + "0d3389c0-5e75-473c-a18b-0699d0911fb2": "performing-administration-tasks-on-the-server-side-with-the-servicerole-secret-BYM4Fa", + "c000b4ea-23e2-4e19-af54-63ccd00c4904": "pg_cron-launcher-crashes-with-duplicate-key-value-violates-unique-constraint-cc6472", + "9a55c946-877f-46ae-8b57-51934e02a36c": "pgcron-debugging-guide-n1KTaz", + "91dda1aa-52f7-4a1c-9248-92fad9f3dec7": "pgrst106-the-schema-must-be-one-of-the-following-error-when-querying-an-exposed-schema", + "f9f9842b-e66c-4ea3-a45b-713895d8b7c6": "pkce-flow-errors-cannot-parse-response-or-zgotmplz-in-magic-link-emails-433665", + "c3481097-1a32-4e94-b4f6-7aeb88132a41": "postgrest-not-recognizing-new-columns-or-functions-bd75f5", + "28d842fd-8ba3-46f0-a307-082d946e0d94": "prisma-error-management-Cm5P_o", + "357e90d6-58a7-4347-a6c0-00622fcf9c81": "project-status-reports-unhealthy-services", + "0dab3e43-2ba9-4421-9af3-b97e45123069": "rclone-error-s3-protocol-error-received-listing-v1-with-istruncated-set-no-nextmarker-and-no-contents-e64d34", + "01e0dd67-5208-4e65-b489-2afd5a0b2af3": "realtime-concurrent-peak-connections-quota-jdDqcp", + "b99fac0e-09da-4a31-93a1-6375ed8ce041": "realtime-connections-timed_out-status", + "4112ff74-dd74-4003-9f4f-479590afed3c": "realtime-debugging-with-logger", + "b826b34a-f7c0-405e-a836-54c543198964": "realtime-handling-silent-disconnections-in-backgrounded-applications-592794", + "e882c2a2-b04c-48d3-9aae-615ec3458477": "realtime-heartbeat-messages", + "3f5acb57-2ccd-4bd7-86c2-df8e23fe2685": "realtime-project-suspended-for-exceeding-quotas", + "dee93cc3-0ab1-4101-8ad4-31d8682c8844": "realtime-too-many-channels-error", + "027fba24-c7bc-4c54-9e6b-6f13ec85ad56": "realtime-warn-sending-broadcast-message", + "b76ac13f-db24-4d19-9d00-af60c978cd26": "refresh-postgrest-schema", + "72e93bc8-597d-4c72-8d86-4997679196fd": "resolving-42p01-relation-does-not-exist-error-W4_9-V", + "031ba6d7-4928-4d95-a2da-bff8dbb740ec": "resolving-500-status-authentication-errors-7bU5U8", + "c89147f8-a66a-4c04-a1a7-45442fd7f2ee": "resolving-database-hostname-and-managing-your-ip-address-pVlwE0", + "bb06ff88-e0a9-41bc-b00b-9afde10468a5": "rls-performance-and-best-practices-Z5Jjwv", + "5b8a5115-f81a-4a1f-bcc3-fdc2a2672e80": "rls-simplified-BJTcS8", + "caa72a34-696c-47c6-8976-e50cf7fb396e": "rotating-anon-service-and-jwt-secrets-1Jq6yd", + "1d62cace-c0f6-47a0-8690-002a797da33b": "running-explain-analyze-on-functions", + "02a26b95-3029-49a1-9319-161cf1c4c21d": "scan-error-on-column-confirmation_token-converting-null-to-string-is-unsupported-during-auth-login-a0c686", + "46bab0a2-9780-4e4f-99f5-fcc9fa51c496": "security-of-anonymous-sign-ins-iOrGCL", + "9faf3ed3-d373-41c8-a083-ccc83dfc16d7": "seeing-no-pghbaconf-entry-for-host-errors-in-postgres-and-they-come-from-an-ip-address-that-i-dont-recognize-4gds9f", + "e51bedf0-cdc4-4880-865b-e39c06dfe497": "should-i-set-a-shorter-max-age-parameter-on-the-cookies-8sbF4V", + "571d4ad9-b596-44bc-b8a4-a3115b862436": "slow-execution-of-alter-table-on-large-table-when-changing-column-type-qmZRpZ", + "c1ca57fb-91ab-4526-987b-65106dc63a2f": "soft-deletes-with-supabase-js", + "37d147df-caa7-4b0b-82b5-eab6852f2cff": "sso-error-you-do-not-have-permissions-to-join-this-organization-or-prompts-to-create-new-organization", + "fb1cbd42-e172-44b2-af2b-fda5aecde5c2": "steps-to-improve-query-performance-with-indexes-q8PoC9", + "f27145c7-0ff5-4621-a364-5d5704bce0ff": "supabase--your-network-ipv4-and-ipv6-compatibility-cHe3BP", + "4a32736f-1d7f-4c41-9c96-ab84e35ad394": "supabase-cli-failed-sasl-auth-or-invalid-scram-server-final-message", + "9d3377c9-fa79-4734-9fa9-8ccc99415f7f": "supabase-dashboard-not-loading-project-not-loading-on-dashboard-LfMq9F", + "179d70f3-1e26-4346-9ee8-d340fad382a3": "supabase-grafana-memory-charts", + "d64f5f5d-ef80-4423-ba24-1a79b4e69ce6": "supabase-project-provisioned-via-bolt-not-visible-in-dashboard-7188fc", + "3b52daf2-d78d-4630-8e9f-8bf5d90208bf": "supabase-storage-inefficient-folder-operations-and-hierarchical-rls-challenges-b05a4d", + "e008e056-453d-44b5-8a8a-27c2d70681fe": "supavisor-and-connection-terminology-explained-9pr_ZO", + "f252ba39-e540-462b-868f-42eb31a5d40c": "supavisor-faq-YyP5tI", + "83446166-2539-44ce-bde5-9252ce59d2e3": "transfer-edge-function-from-one-project-to-another", + "c6b6ae3c-1b5b-4ba8-a40f-5f2ca1f007e2": "transferring-from-cloud-to-self-host-in-supabase-2oWNvW", + "cd298838-6f13-4b70-a198-a8bd26b2c0bd": "unable-to-call-edge-function", + "68bb6df6-6a07-40fa-ad4d-dfa5e21d7e9c": "unable-to-deploy-edge-function", + "0ce5b1e4-bd0a-439e-9fb1-8e27bad2ef10": "understanding-postgresql-explain-output-Un9dqX", + "10186830-8cce-4f10-8cb9-7cbf39310763": "understanding-postgresql-logging-levels-and-how-they-impact-your-project-KXiJRm", + "f379a628-6821-40b0-9793-4bf819b8c085": "understanding-the-usage-summary-on-the-dashboard-D7Gnle", + "e3ad9b0e-0533-444b-a998-c8e65171b529": "unused-external-import-warning-vite-rollup", + "3b0aa0f2-9608-4691-94d6-4fca8a32130e": "upload-file-size-restrictions-Y4wQLT", + "4d5390a0-445d-4e0a-ae7e-9b713b7cc4f6": "using-google-smtp-with-supabase-custom-smtp-ZZzU4Y", + "da4efcda-cd02-4001-965b-f0294300152f": "using-sqlalchemy-with-supabase-FUqebT", + "a5524182-9dc4-4470-86fe-45ace109a53a": "webhook-debugging-guide-M8sk47", + "d4b45e90-9c52-4781-8cc2-d936e8f4edeb": "why-am-i-being-redirected-to-the-wrong-url-when-using-auth-redirectto-option-_vqIeO", + "0c0369db-2968-4ea7-9f99-c5f5ce55acad": "why-are-there-gaps-in-my-postgres-id-sequence-Frifus", + "8abd4a38-041b-4620-95af-53673f698dea": "why-cant-i-uploadlistetc-my-public-bucket-Z6CmGt", + "f85935d8-e871-4463-8288-f118f4e24afd": "why-do-i-see-auth--api-requests-in-the-dashboard-my-app-has-no-users-CyadiO", + "9dbcf760-7b1f-4cc4-8653-4e884fd02dad": "why-is-my-camelcase-name-not-working-in-postgres-functions-or-rls-policies-EJMzVd", + "6fa68831-6db8-4836-a1b4-e9e51296d529": "why-is-my-select-returning-an-empty-data-array-and-i-have-data-in-the-table-xvOPgx", + "677f0a69-e454-4718-ad92-91053d40c085": "why-is-my-service-role-key-client-getting-rls-errors-or-not-returning-data-7_1K9z", + "0a6f6d33-7331-4b47-87ec-7ade7a6a5924": "why-is-my-supabase-api-call-not-returning-PGzXw0", + "99b56e92-62f2-4602-8e13-d38caf7aff4c": "why-supabase-edge-functions-cannot-provide-static-egress-ips-for-whitelisting-3d78b0", + "fa5bb840-a5ac-447f-b9bb-e319cbe0797e": "will-backups-be-accessible-from-the-dashboard-immediately-after-upgrading-to-a-paid-plan-hXY4rs" +} diff --git a/apps/kb/scripts/federated-content/sync-troubleshooting-entries.ts b/apps/kb/scripts/federated-content/sync-troubleshooting-entries.ts new file mode 100644 index 00000000000..c8f307ce190 --- /dev/null +++ b/apps/kb/scripts/federated-content/sync-troubleshooting-entries.ts @@ -0,0 +1,160 @@ +// Creates a GitHub Discussion for every troubleshooting guide that doesn't +// have a troubleshooting_entries row yet. Run after fetch-federated-content.ts, +// from CI only (never kb's own prebuild — this has real side effects). Pass +// --dry-run to log what would be created without creating anything. +import '../utils/dotenv.js' + +import { createHash } from 'node:crypto' +import { readdir, readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { Octokit } from '@octokit/core' +import { retry } from '@octokit/plugin-retry' +import { createClient, type SupabaseClient } from '@supabase/supabase-js' +import matter from 'gray-matter' + +import { githubAuthOptions } from './github-auth.ts' + +const REPOSITORY_ID = 'MDEwOlJlcG9zaXRvcnkyMTQ1ODcxOTM=' // supabase/supabase +const TROUBLESHOOTING_CATEGORY_ID = 'DIC_kwDODMpXOc4CUvEr' // "Troubleshooting" discussion category + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const TROUBLESHOOTING_DIRECTORY = join(SCRIPT_DIR, '../../src/content/troubleshooting') + +const DRY_RUN = process.argv.includes('--dry-run') + +const RetryOctokit = Octokit.plugin(retry) +const octokit = new RetryOctokit(githubAuthOptions()) + +async function readLocalGuides(): Promise> { + const filenames = (await readdir(TROUBLESHOOTING_DIRECTORY)).filter((f) => f.endsWith('.md')) + return Promise.all( + filenames.map(async (filename) => { + const raw = await readFile(join(TROUBLESHOOTING_DIRECTORY, filename), 'utf-8') + const { data, content: body } = matter(raw) + return { slug: filename.replace(/\.md$/, ''), title: data.title as string, body } + }) + ) +} + +function computeChecksum(title: string, body: string): string { + return createHash('sha256').update(JSON.stringify({ title, body })).digest('hex') +} + +async function createDiscussion(title: string, body: string): Promise<{ id: string; url: string }> { + const mutation = ` + mutation CreateDiscussionMutation($repository: ID!, $category: ID!, $title: String!, $body: String!) { + createDiscussion(input: { repositoryId: $repository, categoryId: $category, title: $title, body: $body }) { + discussion { id url } + } + } + ` + const result = await octokit.graphql<{ + createDiscussion: { discussion: { id: string; url: string } } + }>(mutation, { repository: REPOSITORY_ID, category: TROUBLESHOOTING_CATEGORY_ID, title, body }) + return result.createDiscussion.discussion +} + +async function deleteDiscussion(id: string): Promise { + const mutation = ` + mutation DeleteDiscussionMutation($discussionId: ID!) { + deleteDiscussion(input: { discussionId: $discussionId }) { discussion { id } } + } + ` + await octokit.graphql(mutation, { discussionId: id }) +} + +// Small sequential batches, not one unbounded Promise.all — stays clear of +// GitHub's secondary rate limits when many guides are new at once (e.g. the +// first run). Isolates failures per guide instead of letting one abort the +// rest; failures are collected and reported (non-zero exit) at the end. +async function createNewGuides( + db: SupabaseClient, + guides: Array<{ slug: string; title: string; body: string }> +): Promise { + const failures: string[] = [] + const batchSize = 5 + + for (let i = 0; i < guides.length; i += batchSize) { + const batch = guides.slice(i, i + batchSize) + const results = await Promise.allSettled( + batch.map(async (guide) => { + console.log(` - ${guide.slug}`) + if (DRY_RUN) return + + const discussion = await createDiscussion(guide.title, guide.body) + // upsert, not insert: a row can already exist here (matched by slug, + // just without a github_id yet) — onConflict updates it in place + // instead of colliding with the slug unique constraint. + const { error } = await db.from('troubleshooting_entries').upsert( + { + slug: guide.slug, + github_url: discussion.url, + github_id: discussion.id, + checksum: computeChecksum(guide.title, guide.body), + }, + { onConflict: 'slug' } + ) + if (error) { + console.error( + `[sync-troubleshooting-entries] DB upsert failed for ${guide.slug}, rolling back discussion` + ) + await deleteDiscussion(discussion.id) + throw error + } + }) + ) + results.forEach((result, j) => { + if (result.status === 'rejected') { + console.error(`[sync-troubleshooting-entries] Failed for ${batch[j].slug}:`, result.reason) + failures.push(batch[j].slug) + } + }) + } + + return failures +} + +async function syncNewDiscussions() { + const guides = await readLocalGuides() + const db = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string + ) + + const { data: existing, error } = await db + .from('troubleshooting_entries') + .select('slug, github_id') + .in( + 'slug', + guides.map((guide) => guide.slug) + ) + if (error) throw error + + // github_id present means a discussion already exists for that slug — the + // actual "already synced" signal, not mere row presence (a backfilled + // legacy row's slug can exist before it has a discussion). + const githubIdBySlug = new Map((existing ?? []).map((row) => [row.slug, row.github_id])) + const newGuides = guides.filter((guide) => !githubIdBySlug.get(guide.slug)) + + if (newGuides.length === 0) { + console.log('[sync-troubleshooting-entries] No new troubleshooting guides to sync.') + return + } + + console.log( + `[sync-troubleshooting-entries] ${newGuides.length} new guide(s)${DRY_RUN ? ' (dry run)' : ''}:` + ) + const failures = await createNewGuides(db, newGuides) + + console.log( + `[sync-troubleshooting-entries] Synced ${newGuides.length - failures.length}/${newGuides.length} new guide(s).` + ) + if (failures.length > 0) { + throw new Error(`Failed to sync: ${failures.join(', ')}`) + } +} + +syncNewDiscussions().catch((error) => { + throw error +}) diff --git a/apps/kb/scripts/federated-content/sync-troubleshooting-updates.ts b/apps/kb/scripts/federated-content/sync-troubleshooting-updates.ts new file mode 100644 index 00000000000..7230885fa01 --- /dev/null +++ b/apps/kb/scripts/federated-content/sync-troubleshooting-updates.ts @@ -0,0 +1,115 @@ +// Pushes title/content updates to already-synced GitHub Discussions when the +// local troubleshooting guide has changed. Compares a stored checksum against +// a freshly computed one — never the discussion's live content — so this is +// immune to whatever GitHub does internally to stored title/body. A null +// checksum (a row created before this column existed) always counts as +// changed. Updates target the discussion directly via the row's stored +// github_id — no need to list/paginate the category's discussions. Run after +// sync-troubleshooting-entries.ts, from CI only. Pass --dry-run to log +// without writing anything. +import '../utils/dotenv.js' + +import { createHash } from 'node:crypto' +import { readdir, readFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { Octokit } from '@octokit/core' +import { retry } from '@octokit/plugin-retry' +import { createClient } from '@supabase/supabase-js' +import matter from 'gray-matter' + +import { githubAuthOptions } from './github-auth.ts' + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const TROUBLESHOOTING_DIRECTORY = join(SCRIPT_DIR, '../../src/content/troubleshooting') + +const DRY_RUN = process.argv.includes('--dry-run') + +const RetryOctokit = Octokit.plugin(retry) +const octokit = new RetryOctokit(githubAuthOptions()) + +async function readLocalGuides(): Promise> { + const filenames = (await readdir(TROUBLESHOOTING_DIRECTORY)).filter((f) => f.endsWith('.md')) + return Promise.all( + filenames.map(async (filename) => { + const raw = await readFile(join(TROUBLESHOOTING_DIRECTORY, filename), 'utf-8') + const { data, content: body } = matter(raw) + return { slug: filename.replace(/\.md$/, ''), title: data.title as string, body } + }) + ) +} + +function computeChecksum(title: string, body: string): string { + return createHash('sha256').update(JSON.stringify({ title, body })).digest('hex') +} + +async function updateDiscussion(discussionId: string, title: string, body: string): Promise { + const mutation = ` + mutation UpdateDiscussionMutation($discussionId: ID!, $title: String!, $body: String!) { + updateDiscussion(input: { discussionId: $discussionId, title: $title, body: $body }) { + discussion { id } + } + } + ` + await octokit.graphql(mutation, { discussionId, title, body }) +} + +async function syncDiscussionUpdates() { + const guides = await readLocalGuides() + const db = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string + ) + + const { data: rows, error } = await db + .from('troubleshooting_entries') + .select('slug, github_id, checksum') + .in( + 'slug', + guides.map((guide) => guide.slug) + ) + if (error) throw error + + const rowBySlug = new Map((rows ?? []).map((row) => [row.slug, row])) + + let updatedCount = 0 + const failures: string[] = [] + + for (const guide of guides) { + const row = rowBySlug.get(guide.slug) + if (!row) continue // not synced yet — sync-troubleshooting-entries.ts handles that + + const newChecksum = computeChecksum(guide.title, guide.body) + if (row.checksum !== null && row.checksum === newChecksum) continue + + console.log( + `[sync-troubleshooting-updates] Content changed for ${guide.slug}${DRY_RUN ? ' (dry run)' : ''}` + ) + if (DRY_RUN) { + updatedCount++ + continue + } + + try { + await updateDiscussion(row.github_id, guide.title, guide.body) + const { error: updateError } = await db + .from('troubleshooting_entries') + .update({ checksum: newChecksum }) + .eq('slug', guide.slug) + if (updateError) throw updateError + updatedCount++ + } catch (err) { + console.error(`[sync-troubleshooting-updates] Failed to update ${guide.slug}:`, err) + failures.push(guide.slug) + } + } + + console.log(`[sync-troubleshooting-updates] Updated ${updatedCount} discussion(s).`) + if (failures.length > 0) { + throw new Error(`Failed to sync: ${failures.join(', ')}`) + } +} + +syncDiscussionUpdates().catch((error) => { + throw error +}) diff --git a/apps/kb/scripts/generate-markdown.mjs b/apps/kb/scripts/generate-markdown.mjs index ddd6f0237a7..1fa5d27e7e0 100644 --- a/apps/kb/scripts/generate-markdown.mjs +++ b/apps/kb/scripts/generate-markdown.mjs @@ -14,16 +14,16 @@ import { unified } from 'unified' import { visit } from 'unist-util-visit' import YAML from 'yaml' +import { BASE_PATH } from '../src/lib/constants.ts' import { TOPICS, topicToSlug } from '../src/lib/topics.ts' const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)) const CONTENT_DIR = path.join(SCRIPT_DIR, '..', 'src', 'content') const OUTPUT_DIR = path.join(SCRIPT_DIR, '..', 'public', 'markdown') -// Production origin + Astro `base` — used to turn root-relative links into -// full URLs so the exported file still makes sense read on its own. +// Used to turn root-relative links into full URLs so the exported file still +// makes sense read on its own. const SITE_ORIGIN = 'https://supabase.com' -const BASE_PATH = '/kb' // Single processor reused for every file: parses GFM markdown to an mdast // tree and serializes it back, same extensions on both ends so nothing diff --git a/apps/kb/scripts/utils/dotenv.js b/apps/kb/scripts/utils/dotenv.js new file mode 100644 index 00000000000..7913e8a5b00 --- /dev/null +++ b/apps/kb/scripts/utils/dotenv.js @@ -0,0 +1,8 @@ +// Must be a .js not a .ts so it can be imported outside of the scripts +// directory without having to compile + +import dotenv from 'dotenv' + +dotenv.config({ path: '.env.local' }) + +export default undefined diff --git a/apps/kb/src/content.config.ts b/apps/kb/src/content.config.ts index d67a7e17b1f..7f62410d221 100644 --- a/apps/kb/src/content.config.ts +++ b/apps/kb/src/content.config.ts @@ -4,13 +4,38 @@ import { z } from 'astro/zod' import { TOPIC_NAMES } from './lib/topics' +// Format date as "26 SEPTEMBER 2026" +const formatDate = (date: Date): string => { + // Adjust for timezone offset to preserve the intended date + const adjusted = new Date(date.getTime() + date.getTimezoneOffset() * 60000) + return new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + .format(adjusted) + .toUpperCase() +} + // Every entry here is rendered through GuideLayout by +// Shared by every article-like collection (guides, troubleshooting) so their +// schemas can't drift on these fields — both render through ArticleLayout via +// a matching catch-all page and need the same base fields. `description` and +// `topics` are NOT shared: each collection validates those differently below. +const articleFields = { + title: z.string(), + // Surfaces the guide in the homepage's "Featured Guides" section. + pinned: z.boolean().default(false), + github_url: z.string().optional(), +} + +// Every entry here is rendered through ArticleLayout by // src/pages/guides/[...slug].astro — dropping a new file in // src/content/guides doesn't need any per-file layout wiring. const guides = defineCollection({ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/guides' }), schema: z.object({ - title: z.string(), + ...articleFields, description: z.string(), // z.enum (not z.string) so a guide referencing a topic outside TOPICS // fails content validation instead of silently rendering an orphaned tag. @@ -18,7 +43,30 @@ const guides = defineCollection({ // Surfaces the guide in the homepage's "Featured Guides" section. pinned: z.boolean().default(false), github_url: z.string().optional(), + date: z + .date() + .optional() + .transform((date) => (date ? formatDate(date) : undefined)), }), }) -export const collections = { guides } +// Fetched from the private supabase/troubleshooting repo by +// scripts/federated-content/fetch-federated-content.ts (a prebuild step) — +// entirely generated, never hand-authored, see .gitignore. Rendered through +// ArticleLayout by src/pages/troubleshooting/[...slug].astro. +const troubleshooting = defineCollection({ + loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/troubleshooting' }), + schema: z.object({ + ...articleFields, + // The source has no description field, and ArticleLayout already + // renders this conditionally — optional rather than synthesized. + description: z.string().optional(), + // Passed through verbatim from the source repo's own frontmatter, not + // validated against TOPIC_NAMES — this collection's topic vocabulary is + // the source repo's, not kb's, so entries won't all match a kb Topic + // (ArticleLayout and the topic pages degrade gracefully for those). + topics: z.array(z.string()).default([]), + }), +}) + +export const collections = { guides, troubleshooting } diff --git a/apps/kb/src/content/guides/sample-guide.mdx b/apps/kb/src/content/guides/sample-guide.mdx index 9f8b159eef6..c420dd19eea 100644 --- a/apps/kb/src/content/guides/sample-guide.mdx +++ b/apps/kb/src/content/guides/sample-guide.mdx @@ -4,6 +4,7 @@ description: 'A lorem ipsum sample guide exercising every base Markdown element topics: ['Tutorial', 'Database'] pinned: true github_url: 'https://github.com/supabase/supabase/discussions/0000000' +date: 2026-10-05 --- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor diff --git a/apps/kb/src/layouts/ArticleLayout.astro b/apps/kb/src/layouts/ArticleLayout.astro new file mode 100644 index 00000000000..f77d33a416d --- /dev/null +++ b/apps/kb/src/layouts/ArticleLayout.astro @@ -0,0 +1,75 @@ +--- +import { ExternalLink, Github } from 'lucide-react' +import { Badge } from 'ui' + +import Layout from './Layout.astro' +import { getTopicDescription, type Topic, TOPIC_NAMES, topicToSlug } from '../lib/topics' + +interface Props { + title: string + description?: string + // string[], not Topic[]: federated collections (e.g. troubleshooting) pass + // through their source's own topic tags unprocessed, which aren't + // guaranteed to be members of kb's fixed Topic list. + topics?: string[] + github_url?: string + viewGitHubLink?: string + date?: string +} + +const { title, description, topics = [], github_url, viewGitHubLink, date } = Astro.props + +function isKnownTopic(topic: string): topic is Topic { + return (TOPIC_NAMES as readonly string[]).includes(topic) +} +--- + + + + diff --git a/apps/kb/src/layouts/GuideLayout.astro b/apps/kb/src/layouts/GuideLayout.astro deleted file mode 100644 index 0362e842cfa..00000000000 --- a/apps/kb/src/layouts/GuideLayout.astro +++ /dev/null @@ -1,47 +0,0 @@ ---- -import { Github } from 'lucide-react' -import { Badge } from 'ui' - -import Layout from './Layout.astro' -import { getTopicDescription, type Topic, topicToSlug } from '../lib/topics' - -interface Props { - title: string - description?: string - topics?: Topic[] - github_url?: string -} - -const { title, description, topics = [], github_url } = Astro.props ---- - - - - diff --git a/apps/kb/src/layouts/Layout.astro b/apps/kb/src/layouts/Layout.astro index a5f3289967a..959404eef0b 100644 --- a/apps/kb/src/layouts/Layout.astro +++ b/apps/kb/src/layouts/Layout.astro @@ -1,4 +1,5 @@ --- +import { genFaviconLinks } from 'common/MetaFavicons/icons' import Footer from '../components/Footer.astro' import { Header } from '../components/Header' import '../styles/globals.css' @@ -9,6 +10,7 @@ interface Props { } const { title, description } = Astro.props +const faviconRoute = import.meta.env.DEV ? '/favicon/local' : '/favicon' --- @@ -29,27 +31,8 @@ const { title, description } = Astro.props } catch (_) {} })() - - - - - - - - - - - - - - - - - - + {genFaviconLinks(import.meta.env.BASE_URL, faviconRoute).map((link) => )} + diff --git a/apps/kb/src/lib/constants.ts b/apps/kb/src/lib/constants.ts new file mode 100644 index 00000000000..0ae595b3b3f --- /dev/null +++ b/apps/kb/src/lib/constants.ts @@ -0,0 +1,4 @@ +// Matches astro.config.mjs's `base` — the single source of truth for this +// value. Astro pages get it via `import.meta.env.BASE_URL`; anything running +// outside Astro's own pipeline (prebuild scripts) imports it from here. +export const BASE_PATH = '/kb' diff --git a/apps/kb/src/lib/troubleshooting-db.ts b/apps/kb/src/lib/troubleshooting-db.ts new file mode 100644 index 00000000000..d158a2b840c --- /dev/null +++ b/apps/kb/src/lib/troubleshooting-db.ts @@ -0,0 +1,25 @@ +import { createClient } from '@supabase/supabase-js' + +/** + * Looks up each slug's `github_url` from `troubleshooting_entries` (build + * time only — kb is fully static, no SSR). Returns an empty map on any + * failure instead of throwing: this is an optional enhancement (the "Go to + * the GitHub discussion" link), not worth failing the whole build over. + */ +export async function getGithubUrlsBySlug(slugs: string[]): Promise> { + try { + const supabase = createClient( + process.env.SUPABASE_URL as string, + process.env.SUPABASE_SERVICE_ROLE_KEY as string + ) + const { data: rows, error } = await supabase + .from('troubleshooting_entries') + .select('slug, github_url') + .in('slug', slugs) + if (error) throw error + return new Map((rows ?? []).map((row) => [row.slug, row.github_url])) + } catch (error) { + console.warn('[troubleshooting] Failed to look up github_url from the database:', error) + return new Map() + } +} diff --git a/apps/kb/src/pages/guides/[...slug].astro b/apps/kb/src/pages/guides/[...slug].astro index 2ff475f0e76..1b1b208f64f 100644 --- a/apps/kb/src/pages/guides/[...slug].astro +++ b/apps/kb/src/pages/guides/[...slug].astro @@ -2,7 +2,7 @@ import { getCollection, render } from 'astro:content' import { Admonition } from 'ui-patterns/Admonition' -import GuideLayout from '../../layouts/GuideLayout.astro' +import ArticleLayout from '../../layouts/ArticleLayout.astro' export async function getStaticPaths() { const guides = await getCollection('guides') @@ -13,10 +13,13 @@ export async function getStaticPaths() { } const { guide } = Astro.props -const { title, description, topics, github_url } = guide.data +const { title, description, topics, github_url, date } = guide.data const { Content } = await render(guide) + +// guide.filePath is relative to the kb app root, e.g. "src/content/guides/foo.mdx" +const viewGitHubLink = `https://github.com/supabase/supabase/blob/master/apps/kb/${guide.filePath}` --- - + - + diff --git a/apps/kb/src/pages/topics/[topic].astro b/apps/kb/src/pages/topics/[topic].astro index aa741118dc0..7e29d2afdaf 100644 --- a/apps/kb/src/pages/topics/[topic].astro +++ b/apps/kb/src/pages/topics/[topic].astro @@ -6,6 +6,11 @@ import { TOPICS, topicToSlug } from '../../lib/topics' export async function getStaticPaths() { const guides = await getCollection('guides') + const troubleshooting = await getCollection('troubleshooting') + const entries = [ + ...guides.map((guide) => ({ data: guide.data, href: `/guides/${guide.id}` })), + ...troubleshooting.map((entry) => ({ data: entry.data, href: `/troubleshooting/${entry.id}` })), + ] // One page per topic in TOPICS — not just topics a guide already uses — // so every link on the "Topics" nav resolves instead of 404ing. @@ -14,7 +19,7 @@ export async function getStaticPaths() { props: { topic: topic.name, description: topic.description, - guides: guides.filter((guide) => guide.data.topics?.includes(topic.name)), + guides: entries.filter((entry) => entry.data.topics?.includes(topic.name)), }, })) } @@ -33,7 +38,7 @@ const { topic, description, guides } = Astro.props diff --git a/apps/kb/src/pages/troubleshooting/[...slug].astro b/apps/kb/src/pages/troubleshooting/[...slug].astro new file mode 100644 index 00000000000..f3e810e7fc3 --- /dev/null +++ b/apps/kb/src/pages/troubleshooting/[...slug].astro @@ -0,0 +1,29 @@ +--- +import { getCollection, render } from 'astro:content' +import { Admonition } from 'ui-patterns/Admonition' + +import ArticleLayout from '../../layouts/ArticleLayout.astro' +import { getGithubUrlsBySlug } from '../../lib/troubleshooting-db' + +export async function getStaticPaths() { + const troubleshooting = await getCollection('troubleshooting') + + // github_url lives in the DB, not the fetched frontmatter — looked up here + // (build time; kb is fully static, no SSR) rather than baked into the + // fetched files. + const githubUrlBySlug = await getGithubUrlsBySlug(troubleshooting.map((entry) => entry.id)) + + return troubleshooting.map((entry) => ({ + params: { slug: entry.id }, + props: { entry, githubUrl: githubUrlBySlug.get(entry.id) }, + })) +} + +const { entry, githubUrl } = Astro.props +const { title, description, topics } = entry.data +const { Content } = await render(entry) +--- + + + + diff --git a/apps/kb/vercel.json b/apps/kb/vercel.json index e8aa76781b3..1b5f675311a 100644 --- a/apps/kb/vercel.json +++ b/apps/kb/vercel.json @@ -11,6 +11,11 @@ "source": "/kb/topics/:path+.md", "destination": "/kb/markdown/topics/:path+.md", "permanent": true + }, + { + "source": "/kb/troubleshooting/:path+.md", + "destination": "/kb/markdown/troubleshooting/:path+.md", + "permanent": true } ], "rewrites": [ diff --git a/apps/learn/app/favicon.ico b/apps/learn/app/favicon.ico deleted file mode 100644 index 718d6fea483..00000000000 Binary files a/apps/learn/app/favicon.ico and /dev/null differ diff --git a/apps/learn/app/layout.tsx b/apps/learn/app/layout.tsx index abd7a75e38a..c76e2ec5d75 100644 --- a/apps/learn/app/layout.tsx +++ b/apps/learn/app/layout.tsx @@ -14,12 +14,15 @@ const inter = Inter({ subsets: ['latin'] }) const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '' +const FAVICON_ROUTE = process.env.NODE_ENV === 'development' ? '/favicon/local' : '/favicon' + export const metadata: Metadata = { applicationName: 'Learn Supabase', title: 'Learn Supabase', description: 'Learn Supabase.', metadataBase: new URL('https://supabase.com/learn'), - icons: genFaviconData(BASE_PATH), + icons: genFaviconData(BASE_PATH, FAVICON_ROUTE), + manifest: `${BASE_PATH}${FAVICON_ROUTE}/manifest.json`, openGraph: { type: 'article', authors: 'Supabase', diff --git a/apps/learn/components/command.tsx b/apps/learn/components/command.tsx index fdc4064c4fa..dd33a367816 100644 --- a/apps/learn/components/command.tsx +++ b/apps/learn/components/command.tsx @@ -1,6 +1,6 @@ 'use client' -import { motion } from 'framer-motion' +import { motion } from 'motion/react' import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ui' import { CommandCopyButton } from './command-copy-button' diff --git a/apps/learn/package.json b/apps/learn/package.json index 4ad5ea1b9ce..5462d03c490 100644 --- a/apps/learn/package.json +++ b/apps/learn/package.json @@ -19,16 +19,16 @@ }, "dependencies": { "common": "workspace:*", - "framer-motion": "^11.18.2", "jotai": "^2.8.0", "lucide-react": "*", + "motion": "catalog:", "next": "catalog:", "next-themes": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-wrap-balancer": "^1.1.0", "rehype-autolink-headings": "^7.1.0", - "rehype-pretty-code": "^0.9.0", + "rehype-pretty-code": "catalog:", "rehype-slug": "^6.0.0", "remark": "^14.0.3", "remark-code-import": "^1.2.0", @@ -39,13 +39,12 @@ "velite": "catalog:" }, "devDependencies": { - "@shikijs/compat": "^1.1.7", "@types/react": "catalog:", "config": "workspace:^", "eslint-config-supabase": "workspace:*", "mdast-util-toc": "^6.1.1", "npm-run-all": "^4.1.5", - "shiki": "^1.1.7", + "shiki": "catalog:", "tailwindcss": "catalog:", "tsconfig": "workspace:*", "tsx": "catalog:" diff --git a/apps/learn/public/favicon/android-icon-144x144.png b/apps/learn/public/favicon/android-icon-144x144.png deleted file mode 100644 index 144190d1976..00000000000 Binary files a/apps/learn/public/favicon/android-icon-144x144.png and /dev/null differ diff --git a/apps/learn/public/favicon/android-icon-192x192.png b/apps/learn/public/favicon/android-icon-192x192.png index c4f30c5f922..af255b04ff9 100644 Binary files a/apps/learn/public/favicon/android-icon-192x192.png and b/apps/learn/public/favicon/android-icon-192x192.png differ diff --git a/apps/learn/public/favicon/android-icon-36x36.png b/apps/learn/public/favicon/android-icon-36x36.png deleted file mode 100644 index aed5d2b3063..00000000000 Binary files a/apps/learn/public/favicon/android-icon-36x36.png and /dev/null differ diff --git a/apps/learn/public/favicon/android-icon-48x48.png b/apps/learn/public/favicon/android-icon-48x48.png deleted file mode 100644 index 0821ae73784..00000000000 Binary files a/apps/learn/public/favicon/android-icon-48x48.png and /dev/null differ diff --git a/apps/learn/public/favicon/android-icon-512x512.png b/apps/learn/public/favicon/android-icon-512x512.png new file mode 100644 index 00000000000..a95b768d8f7 Binary files /dev/null and b/apps/learn/public/favicon/android-icon-512x512.png differ diff --git a/apps/learn/public/favicon/android-icon-72x72.png b/apps/learn/public/favicon/android-icon-72x72.png deleted file mode 100644 index 10d2bbaaa86..00000000000 Binary files a/apps/learn/public/favicon/android-icon-72x72.png and /dev/null differ diff --git a/apps/learn/public/favicon/android-icon-96x96.png b/apps/learn/public/favicon/android-icon-96x96.png deleted file mode 100644 index a8d96ffed73..00000000000 Binary files a/apps/learn/public/favicon/android-icon-96x96.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-114x114.png b/apps/learn/public/favicon/apple-icon-114x114.png deleted file mode 100644 index 396cee8516a..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-114x114.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-120x120.png b/apps/learn/public/favicon/apple-icon-120x120.png deleted file mode 100644 index 7e4e727910e..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-120x120.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-144x144.png b/apps/learn/public/favicon/apple-icon-144x144.png deleted file mode 100644 index 144190d1976..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-144x144.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-152x152.png b/apps/learn/public/favicon/apple-icon-152x152.png deleted file mode 100644 index cb261b8bba0..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-152x152.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-180x180.png b/apps/learn/public/favicon/apple-icon-180x180.png index 3cf7f1132c8..41f699b14ff 100644 Binary files a/apps/learn/public/favicon/apple-icon-180x180.png and b/apps/learn/public/favicon/apple-icon-180x180.png differ diff --git a/apps/learn/public/favicon/apple-icon-57x57.png b/apps/learn/public/favicon/apple-icon-57x57.png deleted file mode 100644 index 278a7a87bee..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-57x57.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-60x60.png b/apps/learn/public/favicon/apple-icon-60x60.png deleted file mode 100644 index a693570eea3..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-60x60.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-72x72.png b/apps/learn/public/favicon/apple-icon-72x72.png deleted file mode 100644 index 10d2bbaaa86..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-72x72.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-76x76.png b/apps/learn/public/favicon/apple-icon-76x76.png deleted file mode 100644 index 241bd6a2452..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-76x76.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon-precomposed.png b/apps/learn/public/favicon/apple-icon-precomposed.png deleted file mode 100644 index c4f30c5f922..00000000000 Binary files a/apps/learn/public/favicon/apple-icon-precomposed.png and /dev/null differ diff --git a/apps/learn/public/favicon/apple-icon.png b/apps/learn/public/favicon/apple-icon.png deleted file mode 100644 index c4f30c5f922..00000000000 Binary files a/apps/learn/public/favicon/apple-icon.png and /dev/null differ diff --git a/apps/learn/public/favicon/browserconfig.xml b/apps/learn/public/favicon/browserconfig.xml deleted file mode 100644 index 1a8136def88..00000000000 --- a/apps/learn/public/favicon/browserconfig.xml +++ /dev/null @@ -1,2 +0,0 @@ - -#ffffff \ No newline at end of file diff --git a/apps/learn/public/favicon/favicon-128.png b/apps/learn/public/favicon/favicon-128.png deleted file mode 100644 index 4fab8e34978..00000000000 Binary files a/apps/learn/public/favicon/favicon-128.png and /dev/null differ diff --git a/apps/learn/public/favicon/favicon-16x16.png b/apps/learn/public/favicon/favicon-16x16.png deleted file mode 100644 index 7d05a72096b..00000000000 Binary files a/apps/learn/public/favicon/favicon-16x16.png and /dev/null differ diff --git a/apps/learn/public/favicon/favicon-180x180.png b/apps/learn/public/favicon/favicon-180x180.png deleted file mode 100644 index 3cf7f1132c8..00000000000 Binary files a/apps/learn/public/favicon/favicon-180x180.png and /dev/null differ diff --git a/apps/learn/public/favicon/favicon-196x196.png b/apps/learn/public/favicon/favicon-196x196.png deleted file mode 100644 index 1e0258526ae..00000000000 Binary files a/apps/learn/public/favicon/favicon-196x196.png and /dev/null differ diff --git a/apps/learn/public/favicon/favicon-32x32.png b/apps/learn/public/favicon/favicon-32x32.png deleted file mode 100644 index 7eb8cabea60..00000000000 Binary files a/apps/learn/public/favicon/favicon-32x32.png and /dev/null differ diff --git a/apps/learn/public/favicon/favicon-48x48.png b/apps/learn/public/favicon/favicon-48x48.png deleted file mode 100644 index 40377a51b54..00000000000 Binary files a/apps/learn/public/favicon/favicon-48x48.png and /dev/null differ diff --git a/apps/learn/public/favicon/favicon-96x96.png b/apps/learn/public/favicon/favicon-96x96.png deleted file mode 100644 index a8d96ffed73..00000000000 Binary files a/apps/learn/public/favicon/favicon-96x96.png and /dev/null differ diff --git a/apps/learn/public/favicon/favicon.ico b/apps/learn/public/favicon/favicon.ico index 343ad561d71..7a0c86a9c0f 100644 Binary files a/apps/learn/public/favicon/favicon.ico and b/apps/learn/public/favicon/favicon.ico differ diff --git a/apps/learn/public/favicon/local/android-icon-192x192.png b/apps/learn/public/favicon/local/android-icon-192x192.png new file mode 100644 index 00000000000..a174cf0468a Binary files /dev/null and b/apps/learn/public/favicon/local/android-icon-192x192.png differ diff --git a/apps/learn/public/favicon/local/android-icon-512x512.png b/apps/learn/public/favicon/local/android-icon-512x512.png new file mode 100644 index 00000000000..a5fc7d6b949 Binary files /dev/null and b/apps/learn/public/favicon/local/android-icon-512x512.png differ diff --git a/apps/learn/public/favicon/local/apple-icon-180x180.png b/apps/learn/public/favicon/local/apple-icon-180x180.png new file mode 100644 index 00000000000..775ce59ec9b Binary files /dev/null and b/apps/learn/public/favicon/local/apple-icon-180x180.png differ diff --git a/apps/learn/public/favicon/local/favicon.ico b/apps/learn/public/favicon/local/favicon.ico new file mode 100644 index 00000000000..7cc4cd192a9 Binary files /dev/null and b/apps/learn/public/favicon/local/favicon.ico differ diff --git a/apps/learn/public/favicon/local/manifest.json b/apps/learn/public/favicon/local/manifest.json new file mode 100644 index 00000000000..8b55cf1ecb1 --- /dev/null +++ b/apps/learn/public/favicon/local/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Supabase", + "short_name": "Supabase", + "description": "The Postgres Development Platform.", + "display": "standalone", + "theme_color": "#1C1C1C", + "background_color": "#1C1C1C", + "icons": [ + { + "src": "android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/apps/learn/public/favicon/manifest.json b/apps/learn/public/favicon/manifest.json index b3de4e47bae..8b55cf1ecb1 100644 --- a/apps/learn/public/favicon/manifest.json +++ b/apps/learn/public/favicon/manifest.json @@ -7,40 +7,16 @@ "background_color": "#1C1C1C", "icons": [ { - "src": "/favicon/android-icon-36x36.png", - "sizes": "36x36", - "type": "image/png", - "density": "0.75" - }, - { - "src": "/favicon/android-icon-48x48.png", - "sizes": "48x48", - "type": "image/png", - "density": "1.0" - }, - { - "src": "/favicon/android-icon-72x72.png", - "sizes": "72x72", - "type": "image/png", - "density": "1.5" - }, - { - "src": "/favicon/android-icon-96x96.png", - "sizes": "96x96", - "type": "image/png", - "density": "2.0" - }, - { - "src": "/favicon/android-icon-144x144.png", - "sizes": "144x144", - "type": "image/png", - "density": "3.0" - }, - { - "src": "/favicon/android-icon-192x192.png", + "src": "android-icon-192x192.png", "sizes": "192x192", "type": "image/png", - "density": "4.0" + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" } ] } diff --git a/apps/learn/public/favicon/ms-icon-144x144.png b/apps/learn/public/favicon/ms-icon-144x144.png deleted file mode 100644 index 144190d1976..00000000000 Binary files a/apps/learn/public/favicon/ms-icon-144x144.png and /dev/null differ diff --git a/apps/learn/public/favicon/ms-icon-150x150.png b/apps/learn/public/favicon/ms-icon-150x150.png deleted file mode 100644 index bbdf205047c..00000000000 Binary files a/apps/learn/public/favicon/ms-icon-150x150.png and /dev/null differ diff --git a/apps/learn/public/favicon/ms-icon-310x310.png b/apps/learn/public/favicon/ms-icon-310x310.png deleted file mode 100644 index 42bd0698a0f..00000000000 Binary files a/apps/learn/public/favicon/ms-icon-310x310.png and /dev/null differ diff --git a/apps/learn/public/favicon/ms-icon-70x70.png b/apps/learn/public/favicon/ms-icon-70x70.png deleted file mode 100644 index 6a428df84d3..00000000000 Binary files a/apps/learn/public/favicon/ms-icon-70x70.png and /dev/null differ diff --git a/apps/learn/styles/mdx.css b/apps/learn/styles/mdx.css index 8b06fb2d87f..2b54c9738d7 100644 --- a/apps/learn/styles/mdx.css +++ b/apps/learn/styles/mdx.css @@ -8,25 +8,25 @@ display: block; } -[data-rehype-pretty-code-fragment] { +[data-rehype-pretty-code-figure] { @apply relative text-white; } -[data-rehype-pretty-code-fragment] code { +[data-rehype-pretty-code-figure] code { @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0; counter-reset: line; box-decoration-break: clone; } -[data-rehype-pretty-code-fragment] .line { +[data-rehype-pretty-code-figure] [data-line] { @apply px-4 min-h-[1rem] py-0.5 w-full inline-block; } -[data-rehype-pretty-code-fragment] [data-line-numbers] .line { +[data-rehype-pretty-code-figure] [data-line-numbers] [data-line] { @apply px-2; } -[data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before { +[data-rehype-pretty-code-figure] [data-line-numbers] > [data-line]::before { @apply text-foreground-muted text-xs; counter-increment: line; content: counter(line); @@ -36,23 +36,24 @@ text-align: right; } -[data-rehype-pretty-code-fragment] .line--highlighted { +[data-rehype-pretty-code-figure] .line--highlighted { @apply relative bg-surface-200 dark:bg-surface-100; } -[data-rehype-pretty-code-fragment] .line--highlighted { +[data-rehype-pretty-code-figure] .line--highlighted { @apply border-foreground-muted border-l-2; } -[data-rehype-pretty-code-fragment] .line-highlighted span { +[data-rehype-pretty-code-figure] .line-highlighted span { @apply relative; } -[data-rehype-pretty-code-fragment] .word--highlighted { - @apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1; +/* reset default text color */ +[data-rehype-pretty-code-figure] .word--highlighted { + @apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1 text-inherit; } -.dark [data-rehype-pretty-code-fragment] .word--highlighted { +.dark [data-rehype-pretty-code-figure] .word--highlighted { @apply bg-zinc-900; } diff --git a/apps/learn/turbo.jsonc b/apps/learn/turbo.jsonc index d6b9b97dfe5..827f89325c7 100644 --- a/apps/learn/turbo.jsonc +++ b/apps/learn/turbo.jsonc @@ -4,6 +4,7 @@ "tasks": { "build": { "dependsOn": ["^build"], + "env": ["NODE_ENV"], "outputs": ["dist/**", ".next/**", "!.next/cache/**/*", "!.next/dev/**/*", ".velite/**"], }, }, diff --git a/apps/learn/velite.config.js b/apps/learn/velite.config.js index 0e1372c928d..31b269fb05d 100644 --- a/apps/learn/velite.config.js +++ b/apps/learn/velite.config.js @@ -1,5 +1,5 @@ +import { readFile } from 'node:fs/promises' import path from 'path' -import { getHighlighter, loadTheme } from '@shikijs/compat' import rehypeAutolinkHeadings from 'rehype-autolink-headings' import rehypePrettyCode from 'rehype-pretty-code' import rehypeSlug from 'rehype-slug' @@ -8,6 +8,10 @@ import remarkGfm from 'remark-gfm' import { visit } from 'unist-util-visit' import { defineConfig, s } from 'velite' +const codeTheme = JSON.parse( + await readFile(path.join(process.cwd(), 'lib/themes/supabase-2.json'), 'utf8') +) + const LinksProperties = s.object({ doc: s.string().optional(), api: s.string().optional(), @@ -106,10 +110,9 @@ export default defineConfig({ [ rehypePrettyCode, { - getHighlighter: async () => { - const theme = await loadTheme(path.join(process.cwd(), '/lib/themes/supabase-2.json')) - return await getHighlighter({ theme }) - }, + // plugin shares one cached highlighter per theme, lazy language loading needs shiki >= 4.5 (shikijs/shiki#1318) + theme: codeTheme, + keepBackground: false, onVisitLine(node) { // Prevent lines from collapsing in `display: grid` mode, and allow empty // lines to be copy/pasted @@ -118,17 +121,18 @@ export default defineConfig({ } }, onVisitHighlightedLine(node) { - node.properties.className.push('line--highlighted') + // the plugin strips the `line` class first, leaving className undefined + node.properties.className = [...(node.properties.className ?? []), 'line--highlighted'] }, - onVisitHighlightedWord(node) { + onVisitHighlightedChars(node) { node.properties.className = ['word--highlighted'] }, }, ], () => (tree) => { visit(tree, (node) => { - if (node?.type === 'element' && node?.tagName === 'div') { - if (!('data-rehype-pretty-code-fragment' in node.properties)) { + if (node?.type === 'element' && node?.tagName === 'figure') { + if (!('data-rehype-pretty-code-figure' in node.properties)) { return } @@ -137,7 +141,7 @@ export default defineConfig({ return } - preElement.properties['__withMeta__'] = node.children.at(0).tagName === 'div' + preElement.properties['__withMeta__'] = node.children.at(0).tagName === 'figcaption' preElement.properties['__rawString__'] = node.__rawString__ if (node.__src__) { diff --git a/apps/lite-studio/app/root.tsx b/apps/lite-studio/app/root.tsx index b0fab81420b..c2c3ab7e70c 100644 --- a/apps/lite-studio/app/root.tsx +++ b/apps/lite-studio/app/root.tsx @@ -5,6 +5,22 @@ import type { Route } from './+types/root' import '../styles/globals.css' export const links: Route.LinksFunction = () => [ + ...(import.meta.env.DEV + ? [ + { + rel: 'icon', + type: 'image/x-icon', + sizes: '16x16 32x32 48x48', + href: '/favicon/local/favicon.ico', + }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + href: '/favicon/local/apple-icon-180x180.png', + }, + { rel: 'manifest', href: '/favicon/local/manifest.json' }, + ] + : [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]), { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'preconnect', diff --git a/apps/lite-studio/public/favicon/local/android-icon-192x192.png b/apps/lite-studio/public/favicon/local/android-icon-192x192.png new file mode 100644 index 00000000000..a174cf0468a Binary files /dev/null and b/apps/lite-studio/public/favicon/local/android-icon-192x192.png differ diff --git a/apps/lite-studio/public/favicon/local/android-icon-512x512.png b/apps/lite-studio/public/favicon/local/android-icon-512x512.png new file mode 100644 index 00000000000..a5fc7d6b949 Binary files /dev/null and b/apps/lite-studio/public/favicon/local/android-icon-512x512.png differ diff --git a/apps/lite-studio/public/favicon/local/apple-icon-180x180.png b/apps/lite-studio/public/favicon/local/apple-icon-180x180.png new file mode 100644 index 00000000000..775ce59ec9b Binary files /dev/null and b/apps/lite-studio/public/favicon/local/apple-icon-180x180.png differ diff --git a/apps/lite-studio/public/favicon/local/favicon.ico b/apps/lite-studio/public/favicon/local/favicon.ico new file mode 100644 index 00000000000..7cc4cd192a9 Binary files /dev/null and b/apps/lite-studio/public/favicon/local/favicon.ico differ diff --git a/apps/lite-studio/public/favicon/local/manifest.json b/apps/lite-studio/public/favicon/local/manifest.json new file mode 100644 index 00000000000..cb57b9e8531 --- /dev/null +++ b/apps/lite-studio/public/favicon/local/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "Supabase Lite Studio", + "short_name": "Supabase", + "icons": [ + { + "src": "android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "android-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index 48863a87939..49bf873c767 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -3,18 +3,18 @@ "react-hooks/exhaustive-deps": 156, "import/no-anonymous-default-export": 57, "@tanstack/query/exhaustive-deps": 87, - "@typescript-eslint/no-explicit-any": 799, - "no-restricted-exports": 176, + "@typescript-eslint/no-explicit-any": 790, + "no-restricted-exports": 172, "react/no-unstable-nested-components": 34, - "jsx-a11y/control-has-associated-label": 174, + "jsx-a11y/control-has-associated-label": 169, "jsx-a11y/label-has-associated-control": 33, "jsx-a11y/no-redundant-roles": 2, "jsx-a11y/anchor-is-valid": 6, "jsx-a11y/heading-has-content": 2, "valtio/state-snapshot-rule": 1, - "shadcn/no-arbitrary-values": 212, + "shadcn/no-arbitrary-values": 198, "shadcn/no-unknown-classes": 150, - "shadcn/no-raw-colors": 63 + "shadcn/no-raw-colors": 58 }, "ruleFiles": { "react-hooks/exhaustive-deps": { @@ -345,11 +345,10 @@ "components/interfaces/Integrations/Queues/SingleQueue/QueueFilters.tsx": 1, "components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx": 1, "components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx": 1, - "components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx": 2, - "components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx": 3, + "components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx": 1, + "components/interfaces/Integrations/Wrappers/EditWrapperSheet.tsx": 2, "components/interfaces/Integrations/Wrappers/InputField.tsx": 1, "components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx": 3, - "components/interfaces/Integrations/Wrappers/Wrappers.utils.ts": 5, "components/interfaces/JwtSecrets/jwt-secret-keys-table/create-key-dialog.tsx": 2, "components/interfaces/JwtSecrets/jwt-settings.tsx": 1, "components/interfaces/Linter/LinterDataGrid.tsx": 4, @@ -357,7 +356,6 @@ "components/interfaces/Observability/ObservabilityOverview.tsx": 1, "components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx": 3, "components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils.ts": 2, - "components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx": 2, "components/interfaces/Organization/BillingSettings/CreditTopUp.tsx": 2, "components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 3, @@ -666,14 +664,10 @@ "components/interfaces/Integrations/CronJobs/CronJobsEmptyState.tsx": 1, "components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx": 1, "components/interfaces/Integrations/Integration/LegacyIntegrationPage.tsx": 1, - "components/interfaces/Integrations/Wrappers/InputField.tsx": 1, - "components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx": 1, "components/interfaces/Linter/LintPageTabs.tsx": 1, "components/interfaces/Linter/LinterFilters.tsx": 1, "components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx": 1, "components/interfaces/Organization/BillingSettings/BillingEmail.tsx": 1, - "components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx": 1, - "components/interfaces/Organization/BillingSettings/CostControl/SpendCapSidePanel.tsx": 1, "components/interfaces/Organization/BillingSettings/CreditBalance.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/MembersExceedLimitModal.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 1, @@ -845,7 +839,6 @@ "components/interfaces/Auth/Passkeys/PasskeysSettingsForm.tsx": 1, "components/interfaces/Auth/RateLimits/RateLimits.tsx": 1, "components/interfaces/Auth/SessionsAuthSettingsForm/SessionsAuthSettingsForm.tsx": 2, - "components/interfaces/Auth/Users/UserPanel.tsx": 1, "components/interfaces/Auth/Users/UsersFooter.tsx": 1, "components/interfaces/Auth/Users/UsersGridComponents.tsx": 1, "components/interfaces/Auth/Users/UsersSearch.tsx": 1, @@ -913,7 +906,6 @@ "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx": 2, "components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 1, "components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaRow.tsx": 1, - "components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx": 3, "components/interfaces/Settings/Integrations/VercelIntegration/VercelIntegrationConnectionForm.tsx": 3, "components/interfaces/Settings/Logs/MultiSelectActionBar.tsx": 1, "components/interfaces/SignIn/ResetPasswordForm.tsx": 2, @@ -953,7 +945,6 @@ "components/ui/PasswordStrengthBar.tsx": 1, "components/ui/Resource/ResourceItem.tsx": 1, "pages/project/[ref]/auth/templates/[templateId].tsx": 1, - "pages/project/[ref]/branches/merge-requests.tsx": 1, "pages/project/[ref]/observability/auth.tsx": 1, "pages/project/[ref]/observability/edge-functions.tsx": 1, "pages/project/[ref]/observability/postgrest.tsx": 1, @@ -1010,8 +1001,8 @@ "components/grid/components/header/ExportDialog.tsx": 2, "components/interfaces/Account/AccessTokens/Classic/ExperimentalTokenDropdown.tsx": 2, "components/interfaces/Account/AccessTokens/Scoped/Form/RiskMarker.tsx": 2, - "components/interfaces/Account/AuditLogs/AuditLogOverview.tsx": 2, "components/interfaces/Account/Preferences/HotkeyToggle.tsx": 1, + "components/interfaces/AuditLogs/AuditLogOverview.tsx": 2, "components/interfaces/Auth/EmailTemplates/CustomEmailTemplateRestrictionAdmonition.tsx": 2, "components/interfaces/Compute/RuntimeBadge.tsx": 1, "components/interfaces/ConfigDrift/ConfigurationDriftPage.tsx": 1, @@ -1037,7 +1028,6 @@ "components/interfaces/Integrations/Queues/QueueTab.tsx": 1, "components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx": 2, "components/interfaces/Integrations/Warehouse/WarehouseTableStatusList.tsx": 1, - "components/interfaces/Integrations/Wrappers/WrapperRow.tsx": 2, "components/interfaces/LogDrains/OrgAuditLogDrains.tsx": 2, "components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/PlanCard.tsx": 11, @@ -1046,7 +1036,7 @@ "components/interfaces/Organization/Documents/SOC2.tsx": 1, "components/interfaces/Organization/HeaderBanner.tsx": 2, "components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails.tsx": 1, - "components/interfaces/ProjectCreation/RegionSelector.tsx": 1, + "components/interfaces/Organization/OAuthApps/Consent/ScopeGroupCard.tsx": 1, "components/interfaces/ProjectHome/HighAvailabilityBadge.tsx": 5, "components/interfaces/ProjectHome/PlanUsageCard.tsx": 1, "components/interfaces/ProjectHome/ServerLightGrid.tsx": 1, @@ -1066,6 +1056,7 @@ "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx": 2, "components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx": 2, "components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx": 2, + "components/interfaces/Storage/StorageExplorer/ArchivedVersionRestoreWidget.tsx": 1, "components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx": 2, "components/interfaces/Storage/StorageExplorer/MoveItemsFolderPickerRow.tsx": 2, "components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx": 1, @@ -1216,11 +1207,9 @@ "components/interfaces/BranchManagement/DeleteBranchModal.tsx": 1, "components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx": 2, "components/interfaces/Database/Hooks/DeleteHookModal.tsx": 2, - "components/interfaces/Database/Migrations/Migrations.tsx": 1, "components/interfaces/Database/Triggers/DeleteEventTrigger.tsx": 1, "components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx": 2, "components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx": 1, - "components/interfaces/Functions/FunctionsEmptyState.tsx": 1, "components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx": 1, "components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx": 2, "components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue.tsx": 1, @@ -1231,11 +1220,9 @@ "components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx": 1, "components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 1, "components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx": 1, - "components/interfaces/Organization/TeamSettings/MembersView.tsx": 1, "components/interfaces/ProjectCreation/ProjectCreationForm.tsx": 1, "components/interfaces/Reports/renderers/ApiRenderers.tsx": 1, "components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx": 4, - "components/interfaces/Settings/Integrations/IntegrationsSettings.tsx": 1, "components/interfaces/Settings/Logs/Logs.DatePickers.tsx": 1, "components/interfaces/SignIn/ForgotPasswordWizard.tsx": 2, "components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatesList.tsx": 1, @@ -1246,7 +1233,6 @@ "components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts": 1, "components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice.tsx": 1, "components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx": 2, - "components/ui/AIAssistantPanel/Confirm.tsx": 1, "components/ui/AIAssistantPanel/Message.Actions.tsx": 1, "components/ui/AIAssistantPanel/MessagePartQueryLogs.tsx": 1, "components/ui/DatePicker/TimeSplitInput.tsx": 1, diff --git a/apps/studio/AGENTS.md b/apps/studio/AGENTS.md index d007e5dc2bd..19fb5459f2d 100644 --- a/apps/studio/AGENTS.md +++ b/apps/studio/AGENTS.md @@ -83,7 +83,7 @@ Older Studio code predates some of these conventions. For new or modified code, - **Permissions**: `useAsyncCheckPermissions` from `hooks/misc/useCheckPermissions` (returns `can: true` when self-hosted). - **Gating**: `useIsFeatureEnabled` for product features, `useFlag` from `'common'` for feature flags — two different systems. - **Dates**: `dayjs` (plugins pre-loaded at both entries, `pages/_app.tsx` and `routes/__root.tsx`), not `date-fns`. **Toasts**: `toast` from `'sonner'`. -- **Import split**: `'ui'` = primitives, `'ui-patterns'` = composed patterns (`ConfirmationModal`, …), `@ui/*` = alias into `packages/ui/src`. Icons come from `lucide-react`. +- **Import split**: `'ui'` = primitives, `'ui-patterns'` = composed patterns (`ConfirmationModal`, …), `@ui/*` = alias into `packages/ui/src`. Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced). - **New tables** use `@tanstack/react-table`; `react-data-grid` is banned for new code. - **Ad-hoc SQL** against the user's database goes through `executeSql` / `useExecuteSqlMutation` (`data/sql/execute-sql-mutation`). - **Confirmations**: `ConfirmationModal` / `TextConfirmModal` from `ui-patterns`, never `window.confirm`. Disabled buttons needing an explanation use `ButtonTooltip`; inline warnings use `Admonition`. diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index b07f589a098..76f013f044e 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -366,6 +366,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/logout.tsx` ← `pages/logout.tsx` - [x] A `routes/maintenance.tsx` ← `pages/maintenance.tsx` - [x] A `routes/verify-email.tsx` ← `pages/verify-email.tsx` +- [x] A `routes/support/csat.tsx` ← `pages/support/csat.tsx` (pre-auth support satisfaction survey; page brings its own `InterstitialLayout`, so it stays outside `_app/`.) - [x] A `routes/mcp/secrets.tsx` ← `pages/mcp/secrets.tsx` (MCP elicitation URL-mode handoff; page brings its own `InterstitialLayout`, so it stays outside `_app/`.) ### Error pages (handled at root) diff --git a/apps/studio/compat/next/api.test.ts b/apps/studio/compat/next/api.test.ts new file mode 100644 index 00000000000..28f64efd36d --- /dev/null +++ b/apps/studio/compat/next/api.test.ts @@ -0,0 +1,213 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import { describe, expect, it, vi } from 'vitest' + +import { toWebHandler } from './api' + +const createRequest = (body?: string, contentType = 'application/json') => + new Request('http://localhost/api/test', { + method: 'POST', + headers: { 'content-type': contentType }, + body, + }) + +describe('toWebHandler request parsing', () => { + it.each( + ['application/json', 'application/ld+json', 'Application/JSON; charset=utf-8'].flatMap( + (contentType) => + ['{', '[1,', 'undefined', ' ', '{"private":"value",}'].map((body) => ({ + contentType, + body, + })) + ) + )( + 'returns 400 without calling the handler for malformed $contentType: $body', + async ({ body, contentType }) => { + const handler = vi.fn() + const response = await toWebHandler(handler)({ request: createRequest(body, contentType) }) + + expect(response.status).toBe(400) + expect(await response.text()).toBe('Invalid JSON') + expect(handler).not.toHaveBeenCalled() + } + ) + + it.each<[string | undefined, unknown]>([ + ['{"query":"select 1"}', { query: 'select 1' }], + ['[1,"two",null]', [1, 'two', null]], + ['"hello"', 'hello'], + ['42', 42], + ['true', true], + ['false', false], + ['null', null], + ['', {}], + [undefined, {}], + ])('passes valid JSON %s to the handler', async (body, expected) => { + const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => { + expect(req.body).toEqual(expected) + res.status(201).setHeader('x-handler', 'called') + res.json({ success: true }) + }) + + const response = await toWebHandler(handler)({ request: createRequest(body) }) + + expect(handler).toHaveBeenCalledOnce() + expect(response.status).toBe(201) + expect(response.headers.get('x-handler')).toBe('called') + expect(response.headers.get('content-type')).toBe('application/json') + expect(await response.json()).toEqual({ success: true }) + }) + + it.each(['application/json; charset=utf-8', 'application/ld+json', 'Application/JSON'])( + 'accepts JSON content type %s', + async (contentType) => { + const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => res.json(req.body)) + const response = await toWebHandler(handler)({ + request: createRequest('{"name":"café"}', contentType), + }) + + expect(await response.json()).toEqual({ name: 'café' }) + } + ) + + it.each<[string, string, unknown]>([ + [ + 'name=hello+world&value=%26%3D', + 'application/x-www-form-urlencoded', + { + name: 'hello world', + value: '&=', + }, + ], + ['{', 'text/plain', '{'], + ['{', 'text/plain; note="application/json"', '{'], + ['{', 'application/jsonx', '{'], + ])('preserves non-JSON request bodies', async (body, contentType, expected) => { + const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => { + expect(req.body).toEqual(expected) + res.end('ok') + }) + + const response = await toWebHandler(handler)({ request: createRequest(body, contentType) }) + + expect(handler).toHaveBeenCalledOnce() + expect(await response.text()).toBe('ok') + }) + + it.each(['GET', 'HEAD'])('leaves %s bodies undefined', async (method) => { + const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => { + expect(req.method).toBe(method) + expect(req.body).toBeUndefined() + res.end() + }) + + await toWebHandler(handler)({ + request: new Request('http://localhost/api/test', { + method, + headers: { 'content-type': 'application/json' }, + }), + }) + + expect(handler).toHaveBeenCalledOnce() + }) + + it('preserves the URL, headers and route parameter precedence', async () => { + const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => { + expect(req.url).toBe('/api/test?ref=search&tag=one&tag=two&empty=') + expect(req.headers['x-custom']).toBe('value') + expect(req.query).toEqual({ ref: 'route', tag: ['one', 'two'], empty: '' }) + res.end('ok') + }) + + await toWebHandler(handler)({ + request: new Request('http://localhost/api/test?ref=search&tag=one&tag=two&empty=', { + headers: { 'X-Custom': 'value' }, + }), + params: { ref: 'route', omitted: undefined }, + }) + + expect(handler).toHaveBeenCalledOnce() + }) + + it.each(['%', '%ZZ', '%E0%A4%A', '%FF'])( + 'preserves undecodable cookie values: %s', + async (bad) => { + const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => { + expect(req.cookies).toEqual({ bad, good: 'hello world', token: 'a=b', empty: '' }) + res.json({ success: true }) + }) + + const response = await toWebHandler(handler)({ + request: new Request('http://localhost/api/test', { + headers: { cookie: `bad=${bad}; good=hello%20world; token=a=b; empty=` }, + }), + }) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ success: true }) + expect(handler).toHaveBeenCalledOnce() + } + ) + + it('does not hide a body-read failure as invalid JSON', async () => { + const request = createRequest('{}') + const error = new Error('Body read failed') + vi.spyOn(request, 'text').mockRejectedValue(error) + const handler = vi.fn() + + await expect(toWebHandler(handler)({ request })).rejects.toBe(error) + expect(handler).not.toHaveBeenCalled() + }) + + it('does not turn a handler SyntaxError into a request parsing error', async () => { + const error = new SyntaxError('Handler failed') + const handler = vi.fn(() => { + throw error + }) + + await expect(toWebHandler(handler)({ request: createRequest('{}') })).rejects.toBe(error) + expect(handler).toHaveBeenCalledOnce() + }) + + it('passes through a Web Response returned by the handler', async () => { + const expected = new Response('direct', { status: 202, headers: { 'x-direct': 'yes' } }) + + const response = await toWebHandler(() => expected)({ request: createRequest('{}') }) + + expect(response).toBe(expected) + expect(await response.text()).toBe('direct') + }) + + it('keeps streamed responses open for chunks written after the handler returns', async () => { + let finish: (() => void) | undefined + const response = await toWebHandler((_req, res) => { + res.writeHead(202, { 'content-type': 'text/event-stream', 'x-stream': 'yes' }) + res.write('first\n') + finish = () => res.end('last\n') + })({ request: createRequest('{}') }) + + expect(response.status).toBe(202) + expect(response.headers.get('x-stream')).toBe('yes') + const reader = response.body!.getReader() + const decoder = new TextDecoder() + expect(decoder.decode((await reader.read()).value)).toBe('first\n') + finish!() + expect(decoder.decode((await reader.read()).value)).toBe('last\n') + expect((await reader.read()).done).toBe(true) + }) + + it('preserves close and aborted events from the request signal', async () => { + const controller = new AbortController() + const onClose = vi.fn() + const onAborted = vi.fn() + await toWebHandler((req, res) => { + req.on('close', onClose) + req.once('aborted', onAborted) + res.end('ok') + })({ request: new Request('http://localhost/api/test', { signal: controller.signal }) }) + + controller.abort() + + expect(onClose).toHaveBeenCalledOnce() + expect(onAborted).toHaveBeenCalledOnce() + }) +}) diff --git a/apps/studio/compat/next/api.ts b/apps/studio/compat/next/api.ts index 15e46eac39f..5126da3fdfb 100644 --- a/apps/studio/compat/next/api.ts +++ b/apps/studio/compat/next/api.ts @@ -20,9 +20,19 @@ interface RouteCtx { params?: Record } +class InvalidJsonError extends Error {} + export function toWebHandler(handler: NextHandler) { return async ({ request, params = {} }: RouteCtx): Promise => { - const req = await buildRequest(request, params) + let req: NextApiRequest + try { + req = await buildRequest(request, params) + } catch (error) { + if (error instanceof InvalidJsonError) { + return new Response('Invalid JSON', { status: 400, statusText: 'Invalid JSON' }) + } + throw error + } const { res, finalize } = buildResponse() const result = await handler(req, res) @@ -64,7 +74,12 @@ async function buildRequest( const eq = trimmed.indexOf('=') const name = eq >= 0 ? trimmed.slice(0, eq) : trimmed const value = eq >= 0 ? trimmed.slice(eq + 1) : '' - cookies[name] = decodeURIComponent(value) + cookies[name] = value + try { + cookies[name] = decodeURIComponent(value) + } catch { + // Next's cookie parser keeps values that cannot be URI-decoded. + } } } @@ -82,15 +97,23 @@ async function buildRequest( } let body: unknown = undefined - if (method !== 'GET' && method !== 'HEAD' && request.body) { - const contentType = request.headers.get('content-type') ?? '' - if (contentType.includes('application/json')) { + if (method !== 'GET' && method !== 'HEAD') { + const contentType = (request.headers.get('content-type') ?? '') + .split(';', 1)[0] + .trim() + .toLowerCase() + if (contentType === 'application/json' || contentType === 'application/ld+json') { const text = await request.text() - body = text ? JSON.parse(text) : undefined - } else if (contentType.includes('application/x-www-form-urlencoded')) { + try { + // Next treats an empty JSON body as an empty object. + body = text ? JSON.parse(text) : {} + } catch { + throw new InvalidJsonError('Invalid JSON') + } + } else if (request.body && contentType === 'application/x-www-form-urlencoded') { const text = await request.text() body = Object.fromEntries(new URLSearchParams(text)) - } else { + } else if (request.body) { body = await request.text() } } diff --git a/apps/studio/components/grid/components/editor/DateTimeEditor.tsx b/apps/studio/components/grid/components/editor/DateTimeEditor.tsx index 2ffc0353016..ad4ea1b20f3 100644 --- a/apps/studio/components/grid/components/editor/DateTimeEditor.tsx +++ b/apps/studio/components/grid/components/editor/DateTimeEditor.tsx @@ -1,5 +1,5 @@ import dayjs from 'dayjs' -import { ChevronDown } from 'lucide-react' +import { ChevronDownIcon } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import type { RenderEditCellProps } from 'react-data-grid' import { @@ -153,7 +153,7 @@ function BaseEditor({
diff --git a/apps/studio/components/grid/components/editor/TextEditor.tsx b/apps/studio/components/grid/components/editor/TextEditor.tsx index e89d59de7c2..63329bd115d 100644 --- a/apps/studio/components/grid/components/editor/TextEditor.tsx +++ b/apps/studio/components/grid/components/editor/TextEditor.tsx @@ -1,5 +1,5 @@ import { useParams } from 'common' -import { Maximize } from 'lucide-react' +import { MaximizeIcon } from 'lucide-react' import { useCallback, useState } from 'react' import type { RenderEditCellProps } from 'react-data-grid' import { toast } from 'sonner' @@ -180,7 +180,7 @@ export const TextEditor = ({ @@ -119,14 +119,14 @@ function ColumnIcon({ return ( - +

Foreign key relation:

{name}

- +

{foreignKey?.targetTableSchema}.{foreignKey?.targetTableName}. {foreignKey?.targetColumnName} diff --git a/apps/studio/components/grid/components/grid/ColumnOverlayItem.tsx b/apps/studio/components/grid/components/grid/ColumnOverlayItem.tsx index f48b9cedb47..45cf40cb7d2 100644 --- a/apps/studio/components/grid/components/grid/ColumnOverlayItem.tsx +++ b/apps/studio/components/grid/components/grid/ColumnOverlayItem.tsx @@ -1,4 +1,4 @@ -import { Key, Lightbulb, Link, Lock } from 'lucide-react' +import { KeyIcon, LightbulbIcon, LinkIcon, LockIcon } from 'lucide-react' import { useColumnHasIndexSuggestion } from '../../context/TableIndexAdvisorContext' import { ColumnType, SupaColumn } from '../../types' @@ -23,7 +23,7 @@ export function ColumnOverlayItem({ column }: ColumnOverlayItemProps) { {column.isPrimaryKey && (

- +
)} @@ -33,8 +33,10 @@ export function ColumnOverlayItem({ column }: ColumnOverlayItemProps) { {columnFormat} {columnFormat === 'bytea' ? ` (hex)` : ''} - {column.isEncrypted && } - {hasIndexSuggestion && } + {column.isEncrypted && } + {hasIndexSuggestion && ( + + )}
) @@ -44,7 +46,7 @@ function ColumnIcon({ type }: { type: ColumnType }) { switch (type) { case 'foreign_key': // [Joshen] Look into this separately but this should be a hover card instead - return + return default: return null } diff --git a/apps/studio/components/grid/components/grid/GridError.tsx b/apps/studio/components/grid/components/grid/GridError.tsx index f9bcd007a68..39b5f4c83f8 100644 --- a/apps/studio/components/grid/components/grid/GridError.tsx +++ b/apps/studio/components/grid/components/grid/GridError.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' -import { ChevronDown } from 'lucide-react' +import { ChevronDownIcon } from 'lucide-react' import { useCallback } from 'react' import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui' import { Admonition } from 'ui-patterns/Admonition' @@ -224,7 +224,7 @@ const ExpandError = ({ error }: { error: ResponseError }) => {
View error - +
diff --git a/apps/studio/components/grid/components/grid/SelectColumn.tsx b/apps/studio/components/grid/components/grid/SelectColumn.tsx index 3434f3845d1..8a4360e2c55 100644 --- a/apps/studio/components/grid/components/grid/SelectColumn.tsx +++ b/apps/studio/components/grid/components/grid/SelectColumn.tsx @@ -1,4 +1,4 @@ -import { Maximize2 } from 'lucide-react' +import { Maximize2Icon } from 'lucide-react' import { ChangeEvent, InputHTMLAttributes, SyntheticEvent, useEffect, useRef } from 'react' import { CalculatedColumn, @@ -150,7 +150,7 @@ function SelectCellFormatter({ variant="text" size="tiny" className="px-1 rdg-row__select-column__edit-action" - icon={} + icon={} onClick={onEditClick} tooltip={{ content: { diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index d927ec09ce7..e09d4d1d39f 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -1,7 +1,7 @@ import { keepPreviousData } from '@tanstack/react-query' import { useBreakpoint, useParams } from 'common' -import { AnimatePresence, motion } from 'framer-motion' -import { ChevronDown, Trash } from 'lucide-react' +import { ChevronDownIcon, TrashIcon } from 'lucide-react' +import { AnimatePresence, motion } from 'motion/react' import { ReactNode, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { @@ -24,7 +24,10 @@ import { useTableSort } from '@/components/grid/hooks/useTableSort' import type { SupaRow } from '@/components/grid/types' import { GridHeaderActions } from '@/components/interfaces/TableGridEditor/GridHeaderActions' import { isValueTruncated } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' -import { formatTableRowsToSQL } from '@/components/interfaces/TableGridEditor/TableEntity.utils' +import { + formatTableRowsToJSON, + formatTableRowsToSQL, +} from '@/components/interfaces/TableGridEditor/TableEntity.utils' import { useExportAllRowsAsCsv, useExportAllRowsAsJson, @@ -257,7 +260,7 @@ const RowHeader = ({ rows: visibleRows, tableQueriesEnabled = true }: RowHeaderP } else if (type === 'sql') { return formatTableRowsToSQL(snap.table, rows) } else { - return JSON.stringify(rows) + return formatTableRowsToJSON(snap.table, rows) } })() @@ -351,7 +354,7 @@ const RowHeader = ({ rows: visibleRows, tableQueriesEnabled = true }: RowHeaderP {!snap.allRowsSelected ? ( - @@ -378,7 +381,7 @@ const RowHeader = ({ rows: visibleRows, tableQueriesEnabled = true }: RowHeaderP - @@ -430,7 +433,7 @@ const RowHeader = ({ rows: visibleRows, tableQueriesEnabled = true }: RowHeaderP } + icon={} onClick={onRowsDelete} disabled={snap.allRowsSelected && isImpersonatingRole} tooltip={{ diff --git a/apps/studio/components/grid/components/header/RefreshButton.tsx b/apps/studio/components/grid/components/header/RefreshButton.tsx index 5c9cdf67f89..04cd9d08a49 100644 --- a/apps/studio/components/grid/components/header/RefreshButton.tsx +++ b/apps/studio/components/grid/components/header/RefreshButton.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' -import { RefreshCw } from 'lucide-react' +import { RefreshCwIcon } from 'lucide-react' import { Button } from 'ui' import { useTableIndexAdvisor } from '@/components/grid/context/TableIndexAdvisorContext' @@ -34,7 +34,7 @@ export const RefreshButton = ({ tableId, isRefetching }: RefreshButtonProps) =>
-
} className="w-32 justify-start" @@ -73,7 +73,7 @@ const FilterRow = ({ filter, filterIdx, onChange, onDelete, onKeyDown }: FilterR variant="outline" icon={
- +
} > @@ -98,7 +98,7 @@ const FilterRow = ({ filter, filterIdx, onChange, onDelete, onKeyDown }: FilterR @@ -292,7 +292,7 @@ export const SortPopoverPrimitive = ({
@@ -63,7 +63,7 @@ const SortRow = ({ index, columnName, sort, onDelete, onToggle }: SortRowProps)
diff --git a/apps/studio/components/interfaces/APIKeys/CreatePublishableAPIKeyDialog.tsx b/apps/studio/components/interfaces/APIKeys/CreatePublishableAPIKeyDialog.tsx index 12ec00d3f4e..99493b1e13d 100644 --- a/apps/studio/components/interfaces/APIKeys/CreatePublishableAPIKeyDialog.tsx +++ b/apps/studio/components/interfaces/APIKeys/CreatePublishableAPIKeyDialog.tsx @@ -1,5 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { Plus } from 'lucide-react' +import { PlusIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { parseAsString, useQueryState } from 'nuqs' import { useRef } from 'react' @@ -86,7 +86,7 @@ export const CreatePublishableAPIKeyDialog = () => { side="bottom" tooltipOpen={visible === 'publishable' ? false : undefined} > - diff --git a/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx b/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx index 57a57d64f8f..c4d7284c348 100644 --- a/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx +++ b/apps/studio/components/interfaces/APIKeys/CreateSecretAPIKeyDialog.tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useParams } from 'common' -import { Plus, ShieldCheck } from 'lucide-react' +import { PlusIcon, ShieldCheckIcon } from 'lucide-react' import { parseAsString, useQueryState } from 'nuqs' import { useRef } from 'react' import { useForm, type SubmitHandler } from 'react-hook-form' @@ -92,7 +92,7 @@ export const CreateSecretAPIKeyDialog = () => { side="bottom" tooltipOpen={visible === 'secret' ? false : undefined} > - @@ -148,7 +148,7 @@ export const CreateSecretAPIKeyDialog = () => { - + Securing your API key diff --git a/apps/studio/components/interfaces/SignIn/SignInPartner.tsx b/apps/studio/components/interfaces/SignIn/SignInPartner.tsx index 4ddbef11b93..5187661b365 100644 --- a/apps/studio/components/interfaces/SignIn/SignInPartner.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInPartner.tsx @@ -1,4 +1,4 @@ -import { Loader2 } from 'lucide-react' +import { Loader2Icon } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect } from 'react' @@ -48,7 +48,7 @@ export const SignInPartner = () => { return (
- +

Signing in to Supabase Dashboard

By continuing, you agree to Supabase’s{' '} diff --git a/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx b/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx index 0789b140f9a..f8b6949496f 100644 --- a/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInRecoveryCodeForm.tsx @@ -23,6 +23,7 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import z from 'zod' +import { MFA_SUPPORT_EMAIL_HREF } from './SignInMfa.utils' import { AlertError } from '@/components/ui/AlertError' import { useAddLoginEvent } from '@/data/misc/audit-login-mutation' import { useRecoveryCodesStatusQuery } from '@/data/recovery-codes/recovery-codes-status-query' @@ -37,8 +38,6 @@ const schema = z.object({ const formId = 'sign-in-recovery-code-form' -const SUPPORT_EMAIL_HREF = `mailto:support@supabase.com?subject=${encodeURIComponent('Unable to sign in via MFA')}` - export const SignInRecoveryCodeForm = () => { const router = useRouter() const signOut = useSignOut() @@ -112,7 +111,7 @@ export const SignInRecoveryCodeForm = () => { Back to sign in } @@ -238,6 +237,14 @@ export const SignInRecoveryCodeForm = () => { Force sign out and clear cookies +

  • + + Email support to regain access + +
  • diff --git a/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx b/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx index 18e278a8aa5..b2c6617c030 100644 --- a/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInSSOForm.tsx @@ -1,7 +1,7 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { zodResolver } from '@hookform/resolvers/zod' import { useQueryClient } from '@tanstack/react-query' -import { Lock } from 'lucide-react' +import { LockIcon } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useRef, useState } from 'react' @@ -147,7 +147,7 @@ export const SignInWithSSOButton = () => { return ( - diff --git a/apps/studio/components/interfaces/SignIn/SignUpForm.tsx b/apps/studio/components/interfaces/SignIn/SignUpForm.tsx index 9b9ae4cfcb7..a11488cff23 100644 --- a/apps/studio/components/interfaces/SignIn/SignUpForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignUpForm.tsx @@ -1,7 +1,7 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' import { zodResolver } from '@hookform/resolvers/zod' -import { motion } from 'framer-motion' -import { Eye, EyeOff } from 'lucide-react' +import { EyeIcon, EyeOffIcon } from 'lucide-react' +import { motion } from 'motion/react' import { useRouter } from 'next/router' import { parseAsString, useQueryStates } from 'nuqs' import { useRef, useState } from 'react' @@ -193,7 +193,7 @@ export const SignUpForm = ({ onSuccess }: { onSuccess?: () => void }) => { title={passwordHidden ? `Show password` : `Hide password`} aria-label={passwordHidden ? `Show password` : `Hide password`} className="absolute right-1 top-1 px-1.5" - icon={passwordHidden ? : } + icon={passwordHidden ? : } disabled={isSubmitting} onClick={() => setPasswordHidden((prev) => !prev)} /> diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CopyEnvButton.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CopyEnvButton.tsx index 949d5bd483f..0de2eecba51 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CopyEnvButton.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CopyEnvButton.tsx @@ -1,4 +1,4 @@ -import { Copy } from 'lucide-react' +import { CopyIcon } from 'lucide-react' import { useCallback, useState } from 'react' import { toast } from 'sonner' import { Button, copyToClipboard } from 'ui' @@ -39,7 +39,7 @@ export const CopyEnvButton = ({ }, [serverOptions, values]) return ( - ) diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx index cc8c3d86eb3..e05a47d41cb 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructions.tsx @@ -1,5 +1,5 @@ import { useParams } from 'common' -import { Eye, EyeOff } from 'lucide-react' +import { EyeIcon, EyeOffIcon } from 'lucide-react' import { useMemo, useState } from 'react' import { Accordion, @@ -211,7 +211,7 @@ export const CreateTableInstructions = ({ className="w-7" loading={isFetchingSecretValues} onClick={() => setShowKeys(!showKeys)} - icon={showKeys ? : } + icon={showKeys ? : } tooltip={{ content: { side: 'bottom', diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx index b9d9e6b60f7..5b9ddd3cf76 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx @@ -1,5 +1,5 @@ import { useFlag } from 'common' -import { ChevronDown, Plus } from 'lucide-react' +import { ChevronDownIcon, PlusIcon } from 'lucide-react' import { useState } from 'react' import { Button, @@ -29,7 +29,7 @@ export const CreateTableInstructionsDialog = () => {
    ) : (
    diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx index 3a088212579..0c9b8117dec 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx @@ -2,7 +2,15 @@ import { components } from 'api-types' import { useParams } from 'common' import { SqlEditor, TableEditor } from 'icons' import { uniq } from 'lodash' -import { Eye, Loader2, MoreVertical, Pause, Play, Table2, Trash } from 'lucide-react' +import { + EyeIcon, + Loader2Icon, + MoreVerticalIcon, + PauseIcon, + PlayIcon, + Table2Icon, + TrashIcon, +} from 'lucide-react' import Link from 'next/link' import { useMemo, useState } from 'react' import { toast } from 'sonner' @@ -303,7 +311,7 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
    - +

    {table.name}

    @@ -315,7 +323,7 @@ export const TableRowComponent = ({ table, schema, namespace }: TableRowComponen
    {isLoadingPipelineStatus ? ( - + ) : isPipelineRunning ? ( -
    @@ -181,7 +181,7 @@ export const AnalyticsBuckets = () => {
    - diff --git a/apps/studio/components/interfaces/Storage/BroomSparklesIcon.tsx b/apps/studio/components/interfaces/Storage/BroomSparklesIcon.tsx new file mode 100644 index 00000000000..820726f23e0 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/BroomSparklesIcon.tsx @@ -0,0 +1,40 @@ +import { forwardRef, type SVGAttributes } from 'react' + +interface BroomSparklesIconProps extends Omit, 'children'> { + size?: number | string +} + +/** + * Inline copy of Lucide's `broom-sparkles`, which the pinned `lucide-react` predates. + * Takes a `size` prop and strokes in `currentColor`, so it drops in like any Lucide icon. + */ +export const BroomSparklesIcon = forwardRef( + ({ size = 16, className, ...props }, ref) => ( + + + + + + + + + + + + + ) +) +BroomSparklesIcon.displayName = 'BroomSparklesIcon' diff --git a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx index 632268ac572..d487f11d70b 100644 --- a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx +++ b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx @@ -2,9 +2,9 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query' import { useDebounce } from '@uidotdev/usehooks' import { useParams } from 'common' -import { AnimatePresence, motion } from 'framer-motion' import { compact, get, sum, uniqBy } from 'lodash' -import { Upload } from 'lucide-react' +import { UploadIcon } from 'lucide-react' +import { AnimatePresence, motion } from 'motion/react' import { DragEventHandler, useCallback, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { Checkbox, cn } from 'ui' @@ -80,7 +80,7 @@ const DragOverOverlay = ({ className="w-3/4 h-32 border-2 border-dashed border-muted rounded-md flex flex-col items-center justify-center p-6 pointer-events-none" style={{ backgroundColor: 'rgba(0,0,0,0.4)' }} > - +

    Drop your files to upload to this folder

    diff --git a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx index e851762ac4f..2180cbaaf03 100644 --- a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx +++ b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx @@ -2,15 +2,15 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' import { - ArrowLeft, - Check, - ChevronRight, - Columns, - List, - RefreshCw, - Search, - Upload, - X, + ArrowLeftIcon, + CheckIcon, + ChevronRightIcon, + ColumnsIcon, + ListIcon, + RefreshCwIcon, + SearchIcon, + UploadIcon, + XIcon, } from 'lucide-react' import { useRef, useState, type ChangeEvent } from 'react' import { @@ -88,7 +88,7 @@ const HeaderBreadcrumbs = ({ return (
    {idx !== 0 && ( - + )} {isEllipsis ? ( {crumb.name} @@ -217,7 +217,7 @@ export const BucketFilePickerHeader = () => { {breadcrumbs.length > 0 && ( <>
    } + icon={} variant="text" disabled={!canUpdateStorage} loading={isUploading} @@ -342,13 +342,13 @@ export const BucketFilePickerHeader = () => { size="tiny" autoFocus className="w-52" - icon={} + icon={} actions={[
    - void }) =
    {file.name}
    {file.isCorrupted && (
    - +

    File is corrupted, please delete and reupload this file again

    diff --git a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerRow.tsx b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerRow.tsx index a43544d17cf..3b773207abe 100644 --- a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerRow.tsx +++ b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerRow.tsx @@ -1,4 +1,4 @@ -import { AlertCircle, LoaderCircle } from 'lucide-react' +import { AlertCircleIcon, LoaderCircleIcon } from 'lucide-react' import type { CSSProperties, MouseEvent } from 'react' import { Checkbox, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' @@ -101,7 +101,7 @@ export const BucketFilePickerRow = ({ {item.isCorrupted && ( - + File is corrupted, please delete and reupload again. @@ -129,7 +129,7 @@ export const BucketFilePickerRow = ({ } > {item.status === STORAGE_ROW_STATUS.LOADING && ( - { + const isStorageVersioningEnabled = useIsStorageVersioningEnabled() + const versioningState = getBucketVersioningState(bucket) + + if (!isStorageVersioningEnabled || versioningState === 'disabled') return null + + const { variant, state, tooltip } = COPY[versioningState] + + return ( + + + + + + {showPrefix ? `Versioning ${state.toLowerCase()}` : state} + + + + + {tooltip} + + + ) +} diff --git a/apps/studio/components/interfaces/Storage/BucketsPickerDialog/BucketTableRow.tsx b/apps/studio/components/interfaces/Storage/BucketsPickerDialog/BucketTableRow.tsx index 5cb3e449229..0f09095fedf 100644 --- a/apps/studio/components/interfaces/Storage/BucketsPickerDialog/BucketTableRow.tsx +++ b/apps/studio/components/interfaces/Storage/BucketsPickerDialog/BucketTableRow.tsx @@ -1,5 +1,5 @@ import { FilesBucket as FilesBucketIcon } from 'icons' -import { ChevronRight } from 'lucide-react' +import { ChevronRightIcon } from 'lucide-react' import { KeyboardEventHandler, MouseEventHandler } from 'react' import { Badge, cn, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui' @@ -114,7 +114,11 @@ export const BucketTableRow = ({ {!isDisabled && ( <>
    - +
    diff --git a/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx b/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx index b569965dcbd..370f01ae8f0 100644 --- a/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx +++ b/apps/studio/components/interfaces/Storage/FilesBuckets/BucketTable.tsx @@ -1,5 +1,5 @@ import { FilesBucket as FilesBucketIcon } from 'icons' -import { ChevronRight } from 'lucide-react' +import { ChevronRightIcon } from 'lucide-react' import { useRouter } from 'next/navigation' import { Badge, @@ -12,7 +12,10 @@ import { TooltipTrigger, } from 'ui' +import { useIsStorageVersioningEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { BucketVersioningPill } from '@/components/interfaces/Storage/BucketVersioningPill' import { PUBLIC_BUCKET_TOOLTIP } from '@/components/interfaces/Storage/Storage.constants' +import { getBucketVersioningState } from '@/components/interfaces/Storage/StorageVersioning.constants' import { useBucketPolicyCount } from '@/components/interfaces/Storage/useBucketPolicyCount' import { TableRowNoResults } from '@/components/ui/TableRowNoResults' import { @@ -27,12 +30,21 @@ import { createNavigationHandler } from '@/lib/navigation' type BucketTableMode = 'standard' | 'virtualized' +export const useVersioningColumnSpan = () => (useIsStorageVersioningEnabled() ? 1 : 0) + +const BucketVersioningCell = ({ bucket }: { bucket: Bucket }) => { + if (getBucketVersioningState(bucket) === 'disabled') + return - + return +} + type BucketTableHeaderProps = { mode: BucketTableMode hasBuckets?: boolean } export const BucketTableHeader = ({ mode, hasBuckets = true }: BucketTableHeaderProps) => { + const isStorageVersioningEnabled = useIsStorageVersioningEnabled() const BucketTableHeader = mode === 'standard' ? TableHeader : VirtualizedTableHeader const BucketTableRow = mode === 'standard' ? TableRow : VirtualizedTableRow const BucketTableHead = mode === 'standard' ? TableHead : VirtualizedTableHead @@ -51,6 +63,9 @@ export const BucketTableHeader = ({ mode, hasBuckets = true }: BucketTableHeader Policies File size limit Allowed MIME types + {isStorageVersioningEnabled && ( + Versioning + )} Actions @@ -65,22 +80,28 @@ type BucketTableEmptyStateProps = { } export const BucketTableEmptyState = ({ mode, filterString }: BucketTableEmptyStateProps) => { + const versioningColumnSpan = useVersioningColumnSpan() + if (mode === 'standard') { return ( - - ) - } else { - return ( - - -

    No results found

    -

    - Your search for “{filterString}” did not return any results -

    -
    -
    + ) } + + return ( + + +

    No results found

    +

    + Your search for “{filterString}” did not return any results +

    +
    +
    + ) } type BucketTableRowProps = { @@ -98,6 +119,7 @@ export const BucketTableRow = ({ }: BucketTableRowProps) => { const router = useRouter() const { getPolicyCount } = useBucketPolicyCount() + const isStorageVersioningEnabled = useIsStorageVersioningEnabled() const BucketTableRow = mode === 'standard' ? TableRow : VirtualizedTableRow const BucketTableCell = mode === 'standard' ? TableCell : VirtualizedTableCell @@ -158,9 +180,15 @@ export const BucketTableRow = ({

    + {isStorageVersioningEnabled && ( + + + + )} +
    - +